이번 글은 위와 같은 원하는 뷰를 선택하여 해당 뷰에 하이라이트를 주는 오버레이 튜토리얼 커스텀 뷰를 만들어보겠다.
먼저 해당 커스텀뷰는 하단 회색 버튼을 누를 시 두번째 사진처럼 원하는 뷰를 제외한 나머지를 회색으로 가려준다.
이 후 화면을 터치하면 세번째 사진 처럼 설정해 주었던 다음번의 뷰에 하이라이트를 잡아주며 더이상 설정한 곳이 없다면 하이라이트를 종료하게 된다.
MainActivity.kr
package com.example.test
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.databinding.DataBindingUtil
import com.example.test.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.showTutorialButton.setOnClickListener {
showOverlayTutorial()
}
}
private fun showOverlayTutorial() {
val overlayTutorialView = OverlayTutorialView(this).apply {
layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.MATCH_PARENT
)
}
val rootView = binding.root
if (rootView is ViewGroup) {
rootView.addView(overlayTutorialView)
}
val tutorialItems = listOf(
TutorialItem(
view = binding.view1,
description = "This is view 1"
),
TutorialItem(
view = binding.view2,
description = "This is view 2"
),
TutorialItem(
view = binding.view3,
description = "This is view 3"
)
)
overlayTutorialView.setTutorialItems(tutorialItems)
overlayTutorialView.show()
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:background="@color/white">
<TextView
android:id="@+id/view1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="테테테테테"
android:layout_marginStart="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/view2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="테스"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/view3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="테스트"
android:layout_marginEnd="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/show_tutorial_button"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginBottom="30dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
먼저 메인 뷰는 간단하게 버튼 하나와 하이라이트를 줄 뷰 Text1 , Text2, Text3 를 배치하였다.
이제 하이라이트를 주는 OverlayTutorialView를 아래와 같다.
OverlayTutorialView.kr
package com.example.test
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import com.example.test.databinding.OverlayTutorialViewBinding
class OverlayTutorialView(context: Context) : ConstraintLayout(context) {
private lateinit var binding: OverlayTutorialViewBinding
private var currentStep = 0
private lateinit var tutorialItems: List<TutorialItem>
private val transparentHoleView = TransparentHoleView(context)
init {
addView(transparentHoleView)
// binding.closeButton.setOnClickListener { (parent as? ViewGroup)?.removeView(this@OverlayTutorialView) }
// setOnClickListener { showNextStep() }
}
fun setTutorialItems(items: List<TutorialItem>) {
tutorialItems = items
//showNextStep()
}
fun listener() {
binding.closeButton.setOnClickListener { (parent as? ViewGroup)?.removeView(this@OverlayTutorialView) }
setOnClickListener { showNextStep() }
}
fun show() {
showNextStep()
binding = OverlayTutorialViewBinding.inflate(LayoutInflater.from(context), this, true)
listener()
}
private fun showNextStep() {
if (currentStep < tutorialItems.size) {
val currentItem = tutorialItems[currentStep]
highlightView(currentItem.view)
//binding.descriptionTextView.text = currentItem.description
currentStep++
} else {
(parent as? ViewGroup)?.removeView(this)
}
}
private fun highlightView(view: View) {
// Get the position of the view to highlight
val location = IntArray(2)
view.getLocationOnScreen(location)
// Get the position of the OverlayTutorialView
val parentLocation = IntArray(2)
getLocationOnScreen(parentLocation)
// Calculate the position of the TransparentHoleView relative to the screen
val x = location[0].toFloat() - parentLocation[0]
val y = location[1].toFloat() - parentLocation[1]
// Convert 4dp margin to pixels
val margin = 4 * resources.displayMetrics.density
// Set the size and position of the TransparentHoleView to match the highlighted view
// with an additional margin of 4dp
val width = view.width
val height = view.height
val holeBounds = RectF(
x - margin,
y - margin,
x + width + margin,
y + height + margin
)
transparentHoleView.setHoleBounds(holeBounds)
}
}
data class TutorialItem(
val view: View,
val description: String
)
overlay_tutorial_view
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/description_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:padding="16dp"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:padding="8dp"
android:background="#FF03DAC5"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
간단히 설명하면
- OverlayTutorialView는 TutorialItem 객체의 목록을 유지한다. 각 TutorialItem은 뷰와 설명을 포함한다.
- showNextStep() 함수는 다음 단계의 튜토리얼을 표시하는 역할을 한다. 현재 단계에 해당하는 뷰를 강조 표시하기 위해 TransparentHoleView를 사용하고 설명 텍스트를 업데이트한다. 더 이상 단계가 없는 경우, OverlayTutorialView는 부모로부터 제거된다.
- highlightView() 함수는 강조할 뷰의 위치와 크기를 계산하고 TransparentHoleView의 경계를 설정한다. TransparentHoleView는 배경을 투명하게 만들고 강조된 뷰를 보여줄 수 있도록 하는 커스텀 뷰이다.
- setTutorialItems() 함수는 튜토리얼 아이템 목록을 설정하는 데 사용된다.
- listener() 함수는 닫기 버튼과 OverlayTutorialView 자체에 대한 클릭 리스너를 설정하여 사용자 상호작용에 대응한다.
- OverlayTutorialView는 제공된 튜토리얼 아이템과 함께 튜토리얼 오버레이를 표시하기 위해 show() 함수를 호출하여 사용된다.
이제 이 뷰에 가장 중요한 TranspaentHoleView를 살펴보자.
package com.example.test
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class TransparentHoleView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
alpha = 128
}
private var holeBounds: RectF? = null
fun setHoleBounds(bounds: RectF) {
holeBounds = bounds
invalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// Draw the semi-transparent black background
canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint)
// Draw the completely transparent hole
holeBounds?.let {
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
// Convert 16dp to pixels
val cornerRadius = 16 * resources.displayMetrics.density
canvas?.drawRoundRect(it, cornerRadius, cornerRadius, paint)
paint.xfermode = null
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
}
이 뷰는 배경을 투명하게 만들고 특정 영역을 뚫는 역할을 한다.
- TransparentHoleView는 setHoleBounds 함수를 사용하여 투명한 영역의 경계를 설정한다. 이 함수는 RectF 타입의 경계를 받아 설정하고 화면을 다시 그리도록 invalidate() 함수를 호출한다.
- onAttachedToWindow 함수는 뷰가 윈도우에 첨부될 때 호출된다. 이 함수에서는 소프트웨어 가속을 비활성화하기 위해 setLayerType 함수를 사용한다.
이제 메인 액티비티에서 원하는 뷰와 Text를 설정하면 완성이다.
private fun showOverlayTutorial() {
val overlayTutorialView = OverlayTutorialView(this).apply {
layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.MATCH_PARENT
)
}
val rootView = binding.root
if (rootView is ViewGroup) {
rootView.addView(overlayTutorialView)
}
val tutorialItems = listOf(
TutorialItem(
view = binding.view1,
description = "This is view 1"
),
TutorialItem(
view = binding.view2,
description = "This is view 2"
),
TutorialItem(
view = binding.view3,
description = "This is view 3"
)
)
overlayTutorialView.setTutorialItems(tutorialItems)
overlayTutorialView.show()
}
'Android > 커스텀 뷰' 카테고리의 다른 글
디자인 시스템의 텍스트 크기 문제 해결 경험 (0) | 2024.03.23 |
---|---|
CustomBottomSheet (0) | 2023.10.14 |
커스텀 뷰에 Depth를 줄여보자 (0) | 2023.04.13 |
둥근 모서리를 가진 그라데이션 테두리 사각형 만들기 (0) | 2023.03.29 |
커스텀 뷰 만들어보자 (0) | 2022.12.30 |