[Android] Kotlin Compose 활용해서 Tooltip 직접 구현하기

2024. 5. 21. 20:28· Development/Android
반응형

안드로이드에서 Balloon 툴팁 구현하실 때, 아마 알고 계신 분들도 많겠지만 해당 라이브러리를 사용하는 경우가 많습니다.

https://github.com/skydoves/Balloon

 

GitHub - skydoves/Balloon: :balloon: Modernized and sophisticated tooltips, fully customizable with an arrow and animations for

:balloon: Modernized and sophisticated tooltips, fully customizable with an arrow and animations for Android. - skydoves/Balloon

github.com

 

기존 View 방식이나 Compose 방식을 모두 활용할 수 있기에 간단하게 사용하실 분들은 바로 이용하셔도 좋겠습니다.

저는 조금 특수하게 커스터마이징을 해야 할 일이 있어서 직접 구현에 도전해 보게 되었습니다.

구현에 앞서서 먼저 혹시 Compose에서 자체적으로 제공하는 컴포넌트가 없을까 하고 찾아보니 Materal3에서 TootipBox라는 컴포넌트를 지원하고 있더군요!

 

@Composable
@ExperimentalMaterial3Api
fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable CaretScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
) {
    @Suppress("DEPRECATION")
    val transition = updateTransition(state.transition, label = "tooltip transition")
    var anchorBounds: LayoutCoordinates? by remember { mutableStateOf(null) }
    val scope = remember {
        object : CaretScope {
            override fun Modifier.drawCaret(
                draw: CacheDrawScope.(LayoutCoordinates?) -> DrawResult
            ): Modifier =
                this.drawWithCache { draw(anchorBounds) }
        }
    }

    val wrappedContent: @Composable () -> Unit = {
        Box(
            modifier = Modifier.onGloballyPositioned { anchorBounds = it }
        ) {
            content()
        }
    }

    BasicTooltipBox(
        positionProvider = positionProvider,
        tooltip = { Box(Modifier.animateTooltip(transition)) { scope.tooltip() } },
        focusable = focusable,
        enableUserInput = enableUserInput,
        state = state,
        modifier = modifier,
        content = wrappedContent
    )
}

 

해당 Compose 컴포넌트를 활용하여 아래와 같은 화면 기능을 구성해보겠습니다.

 

먼저 툴팁이 표시될 상하좌우 방향에 대해 enum으로 선언합니다.

enum class TooltipDirection {
    Top, Bottom, Left, Right
}

 

그리고 툴팁 컴포저블 함수는 해당 enum을 파라미터로 받습니다.

@Composable
fun CustomTooltipBox(
    direction: TooltipDirection
) { ... }

 

툴팁의 showing 상태와 실행을 지원해 줄 remember를 선언합니다.

val tooltipState = remember { TooltipState() }
val coroutineScope = rememberCoroutineScope()

 

이제 TooltipBox()를 구성합니다. 

state에는 앞에서 선언한 tooltipState remember를, positionProvider에는 툴팁 팝업 위치 속성을 조정 가능한 PopupPositionProvider를 넣어줘야 합니다. 

tooltip에는 구현하고자 하는 툴팁의 레이아웃을 구성해 주고, content에서는 툴팁 효과를 호출시킬 대상 View 또는 레이아웃울 구성합니다.

TooltipBox(
    state = tooltipState,
    positionProvider = remember {
        CustomTooltipPositionProvider(direction)
    },
    tooltip = {
        when (direction) {
            TooltipDirection.Top -> {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Box(
                        modifier = Modifier
                            .clip(RoundedCornerShape(8.dp))
                            .background(Color.DarkGray)
                            .padding(horizontal = 8.dp, vertical = 4.dp)
                    ) {
                        Text(
                            text = "툴팁 텍스트",
                            modifier = Modifier.padding(8.dp),
                            color = Color.White,
                        )
                    }
                    Canvas(modifier = Modifier.size(10.dp)) {
                        drawTooltipArrow(this, Color.DarkGray, direction)
                    }
                }
            }
            TooltipDirection.Bottom -> {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Canvas(modifier = Modifier.size(10.dp)) {
                        drawTooltipArrow(this, Color.DarkGray, direction)
                    }
                    Box(
                        modifier = Modifier
                            .clip(RoundedCornerShape(8.dp))
                            .background(Color.DarkGray)
                            .padding(horizontal = 8.dp, vertical = 4.dp)
                    ) {
                        Text(
                            text = "툴팁 텍스트",
                            modifier = Modifier.padding(8.dp),
                            color = Color.White,
                        )
                    }
                }
            }
            TooltipDirection.Left -> {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Box(
                        modifier = Modifier
                            .clip(RoundedCornerShape(8.dp))
                            .background(Color.DarkGray)
                            .padding(horizontal = 8.dp, vertical = 4.dp)
                    ) {
                        Text(
                            text = "툴팁 텍스트",
                            modifier = Modifier.padding(8.dp),
                            color = Color.White,
                        )
                    }
                    Canvas(modifier = Modifier.size(10.dp)) {
                        drawTooltipArrow(this, Color.DarkGray, direction)
                    }
                }
            }
            TooltipDirection.Right -> {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Canvas(modifier = Modifier.size(10.dp)) {
                        drawTooltipArrow(this, Color.DarkGray, direction)
                    }
                    Box(
                        modifier = Modifier
                            .clip(RoundedCornerShape(8.dp))
                            .background(Color.DarkGray)
                            .padding(horizontal = 8.dp, vertical = 4.dp)
                    ) {
                        Text(
                            text = "툴팁 텍스트",
                            modifier = Modifier.padding(8.dp),
                            color = Color.White,
                        )
                    }
                }
            }
        }
    },
    content = {
        Box(
            modifier = Modifier
                .background(Color.Gray)
                .padding(16.dp)
                .clickable {
                    coroutineScope.launch { tooltipState.show() }
                }
        ) {
            Text("Hover me")
        }
    }
)

 

content는 간단하게 Text Button을 구성해 줬고, 

tooltip 레이아웃은 Direction 마다 달리 가져 다양한 방향으로 툴팁이 보이도록 했습니다.

 

tooltip = {
    when (direction) {
        TooltipDirection.Top -> {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Box(
                    modifier = Modifier
                        .clip(RoundedCornerShape(8.dp))
                        .background(Color.DarkGray)
                        .padding(horizontal = 8.dp, vertical = 4.dp)
                ) {
                    Text(
                        text = "툴팁 텍스트",
                        modifier = Modifier.padding(8.dp),
                        color = Color.White,
                    )
                }
                Canvas(modifier = Modifier.size(10.dp)) {
                    drawTooltipArrow(this, Color.DarkGray, direction)
                }
            }
        }
        TooltipDirection.Bottom -> {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Canvas(modifier = Modifier.size(10.dp)) {
                    drawTooltipArrow(this, Color.DarkGray, direction)
                }
                Box(
                    modifier = Modifier
                        .clip(RoundedCornerShape(8.dp))
                        .background(Color.DarkGray)
                        .padding(horizontal = 8.dp, vertical = 4.dp)
                ) {
                    Text(
                        text = "툴팁 텍스트",
                        modifier = Modifier.padding(8.dp),
                        color = Color.White,
                    )
                }
            }
        }
        TooltipDirection.Left -> {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Box(
                    modifier = Modifier
                        .clip(RoundedCornerShape(8.dp))
                        .background(Color.DarkGray)
                        .padding(horizontal = 8.dp, vertical = 4.dp)
                ) {
                    Text(
                        text = "툴팁 텍스트",
                        modifier = Modifier.padding(8.dp),
                        color = Color.White,
                    )
                }
                Canvas(modifier = Modifier.size(10.dp)) {
                    drawTooltipArrow(this, Color.DarkGray, direction)
                }
            }
        }
        TooltipDirection.Right -> {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Canvas(modifier = Modifier.size(10.dp)) {
                    drawTooltipArrow(this, Color.DarkGray, direction)
                }
                Box(
                    modifier = Modifier
                        .clip(RoundedCornerShape(8.dp))
                        .background(Color.DarkGray)
                        .padding(horizontal = 8.dp, vertical = 4.dp)
                ) {
                    Text(
                        text = "툴팁 텍스트",
                        modifier = Modifier.padding(8.dp),
                        color = Color.White,
                    )
                }
            }
        }
    }
},

 

화살표 모양 같은 경우에는 Compose Canvas를 활용했고, 마찬가지로 Direction을 파라미터로 받아
매번 다르게 draw 하도록 구현했습니다.

Canvas(modifier = Modifier.size(10.dp)) {
    drawTooltipArrow(this, Color.DarkGray, direction)
}

.
.
.

fun DrawScope.drawTooltipArrow(scope: DrawScope, color: Color, direction: TooltipDirection) {
    val path = Path().apply {
        when (direction) {
            TooltipDirection.Top -> {
                moveTo(0f, 0f)
                lineTo(size.width / 2, size.height)
                lineTo(size.width, 0f)
            }
            TooltipDirection.Bottom -> {
                moveTo(0f, size.height)
                lineTo(size.width / 2, 0f)
                lineTo(size.width, size.height)
            }
            TooltipDirection.Left -> {
                moveTo(size.width, size.height / 2)
                lineTo(0f, 0f)
                lineTo(0f, size.height)
            }
            TooltipDirection.Right -> {
                moveTo(0f, size.height / 2)
                lineTo(size.width, 0f)
                lineTo(size.width, size.height)
            }
        }
        close()
    }
    scope.drawPath(
        path = path,
        color = color
    )
}

DrawScope는 Jetpack Compose에서 제공하는 API로, 사용자 정의 그리기 작업을 수행할 수 있게 해줍니다.

DrawScope를 사용하면 Canvas 컴포저블 내에서 직접 도형을 그리거나, 복잡한 커스텀 렌더링 로직을 구현할 수 있습니다.

 

마지막으로 PopupPositionProvider를 상속받아 TooltipPositionProvider를 구현합니다.

TooltipPositionProvider는 Jetpack Compose에서 툴팁(Tooltip)의 위치를 결정하는 데 사용되는 인터페이스입니다.

툴팁이 어떤 기준으로 어디에 나타날지를 정의하는 역할을 합니다. 이를 통해 툴팁이 부모 요소의 상단, 하단, 좌측, 우측 등 원하는 위치에 정확히 나타나도록 제어할 수 있습니다.

class CustomTooltipPositionProvider(private val direction: TooltipDirection) : PopupPositionProvider {
    override fun calculatePosition(
        anchorBounds: IntRect,
        windowSize: IntSize,
        layoutDirection: LayoutDirection,
        popupContentSize: IntSize
    ): IntOffset {
        val x: Int
        val y: Int
        when (direction) {
            TooltipDirection.Top -> {
                x = anchorBounds.left + (anchorBounds.width / 2) - (popupContentSize.width / 2)
                y = anchorBounds.top - popupContentSize.height
            }
            TooltipDirection.Bottom -> {
                x = anchorBounds.left + (anchorBounds.width / 2) - (popupContentSize.width / 2)
                y = anchorBounds.bottom
            }
            TooltipDirection.Left -> {
                x = anchorBounds.left - popupContentSize.width
                y = anchorBounds.top + (anchorBounds.height / 2) - (popupContentSize.height / 2)
            }
            TooltipDirection.Right -> {
                x = anchorBounds.right
                y = anchorBounds.top + (anchorBounds.height / 2) - (popupContentSize.height / 2)
            }
        }
        return IntOffset(x, y)
    }
}

 

이제 아래와 같이 동작하는 것을 확인할 수 있습니다.

반응형
저작자표시 (새창열림)
'Development/Android' 카테고리의 다른 글
  • [Android / Compose] Skeleton Shimmer Effect 처리
  • [Android / Compose] java.lang.illegalstateexception: viewtreelifecycleowner not found from android.widget.framelayout in ~ 대응
  • [Android] Compose에서 exoPlayer 활용 mp4 영상 재생
  • [Kotlin] 반올림 처리 - DecimalFormat
SeungYong.Lee
SeungYong.Lee
반응형
SeungYong.Lee
Win-Dev
SeungYong.Lee
전체
오늘
어제
  • All (235) N
    • Development (135) N
      • Android (131) N
      • iOS (0)
      • Flutter (4)
      • Backend (0)
    • Algorithm (5)
    • Knowledge (5)
      • IT (2)
      • Science (0)
      • ETC & Tip (3)
    • Today I Learn (28)
    • Coding Test (62)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

  • 안녕하세요. 반갑습니다 :)

인기 글

태그

  • hilt
  • compose
  • Kotlin
  • Imageview
  • 코틀린
  • coroutine
  • 코딩테스트
  • exception
  • Collection
  • Java
  • glance
  • Android
  • Widget
  • Retrofit
  • HTTP
  • Flutter
  • 프로그래머스
  • Animation
  • 비동기처리
  • dfs

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
SeungYong.Lee
[Android] Kotlin Compose 활용해서 Tooltip 직접 구현하기
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.