Foggy day

[Android] Overlay View 그리기 본문

Android

[Android] Overlay View 그리기

jinhan38 2024. 3. 12. 11:59

 

Android에서 Overlay 기능이 필요할 때가 있습니다. 예를 들어 네비게이션 앱처럼 앱을 백그라운드 상태로 두더라도 화면에 특정 View를 보여주는 것입니다. 이럴 때 사룔하는 것이 Overlay 기능입니다. 다른 앱 위에 우리가 설정한 View를 보여주고, 특정 로직을 수행할 수 있습니다. 이를 위해서는 권한 요청과 Service 클래스를 구현해야 합니다. 

Overlay를 사용할 때 주의할 것은 앱을 종료시키더라도 Overlay가 같이 종료되는 것은 아니라는 점입니다. 때문에 비즈니스 로직에 따라서 어떠한 경우에 overlay를 종료시킬지, 계속 살려둘지를 잘 설정해야 합니다. 또한 Service만 종료시키는 것이 아니라 View들도 직접 제거해줘야 합니다. 

 

예제에서 구현한 기능

 

1. Overlay 권한 획득

2. Overlay Service 호출

3. OverlayService에서 View 추가 및 사이즈 변경 

4. Overlay 종료 프로세스 

 

 

 

 

자세한 내용은 코드에 주석으로 추가했습니다. 

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <application
       
       ... 
       
        <activity
            android:name=".OverlayActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".OverlayBoxService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.SYSTEM_ALERT_WINDOW" />
            
    </application>

</manifest>

 

 

OverlayActivity

class OverlayActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_overlay)
        findViewById<Button>(R.id.bt_show_overlay).setOnClickListener {
            checkOverlayPermission()
        }
    }

    /**
     * 권한 요청
     */
    private fun checkOverlayPermission() {
        if (!Settings.canDrawOverlays(this)) {
            val intent = Intent(
                Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:$packageName")
            )
            activityResultLauncher.launch(intent)
        } else {
            showOverlay()
        }
    }

    /**
     * 권한 요청 리스너
     */
    private val activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) {
        if (Settings.canDrawOverlays(this)) {
            showOverlay()
        }
    }

    /**
     * Overlay 서비스 호출
     */
    private fun showOverlay() {
        val serviceIntent = Intent(this, OverlayBoxService::class.java)
        startService(serviceIntent)
    }

    /**
     * 앱을 종료시킬 때 Service도 같이 종료시키는 로직
     */
    override fun onDestroy() {
        val serviceIntent = Intent(this, OverlayBoxService::class.java)
        stopService(serviceIntent)
        super.onDestroy()
    }
}

 

activity_overlay

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".OverlayActivity">

    <Button
        android:id="@+id/bt_show_overlay"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Show Overlay"
        android:layout_marginBottom="15dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

OverlayBoxService 

api레벨에 따른 분기처리도 추가했습니다. 

import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.Button


class OverlayBoxService : Service() {
    companion object {
        private const val TAG = "OverlayBoxService"
    }

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }


    private lateinit var windowManager: WindowManager

    private var screenWidth = 0
    private var screenHeight = 0
    private var flag: Int = 0

    private lateinit var boxView: View
    private lateinit var changeButton: Button
    private lateinit var closeButton: Button

    override fun onCreate() {
        super.onCreate()
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        getScreenSize()
        setParamFlag()
        setBoxView()
        setSizeChangeButton()
        setCloseOverlayButton()
    }

    /**
     * api 레벨에 따라서 화면 사이즈 불러오기
     */
    private fun getScreenSize() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val screenRect = windowManager.currentWindowMetrics.bounds
            screenWidth = screenRect.right
            screenHeight = screenRect.bottom
        } else {
            val metrics = DisplayMetrics()
            windowManager.defaultDisplay.getMetrics(metrics)
            screenWidth = metrics.widthPixels
            screenHeight = metrics.heightPixels
        }
    }

    /**
     * api 레벨에 따라서 flag 설정
     */
    private fun setParamFlag() {
        flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
        }
    }

    /**
     * BoxView 세팅
     */
    private fun setBoxView() {
        boxView = View(this)
        boxView.setBackgroundColor(Color.BLUE)
        val params = WindowManager.LayoutParams(
            screenWidth / 2,
            screenHeight / 3,
            flag,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )
        params.gravity = Gravity.CENTER
        windowManager.addView(boxView, params)

    }

    /**
     * BoxView 사이즈 변경하는 버튼 세팅
     */
    private fun setSizeChangeButton() {
        changeButton = Button(this)
        changeButton.text = "사이즈 변경"
        changeButton.setOnClickListener {
            changeSize(screenWidth / 5, screenHeight / 5)
        }
        val params = WindowManager.LayoutParams(
            screenWidth,
            dpToPx(60, this),
            flag,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )
        params.y = dpToPx(75, this)
        params.gravity = Gravity.BOTTOM or Gravity.START
        windowManager.addView(changeButton, params)
    }

    /**
     * Overlay 종료 버튼
     */
    private fun setCloseOverlayButton() {
        closeButton = Button(this)
        closeButton.text = "종료"
        closeButton.setOnClickListener {
            removeViews()
            stopSelf()
        }
        val params = WindowManager.LayoutParams(
            screenWidth,
            dpToPx(60, this),
            flag,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )
        params.y = dpToPx(135, this)
        params.gravity = Gravity.BOTTOM or Gravity.START
        windowManager.addView(closeButton, params)

    }

    /**
     * dp를 px 사이즈로 변경
     */
    private fun dpToPx(dp: Int, context: Context): Int {
        val density = context.resources.displayMetrics.density
        return (dp * density + 0.5f).toInt()
    }

    /**
     * BoxView 사이즈 변경
     */
    private fun changeSize(width: Int, height: Int) {
        val params = WindowManager.LayoutParams(
            width,
            height,
            flag,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )
        windowManager.updateViewLayout(boxView, params)

    }

    /**
     * OverlayActivity에서 onDestroy 가 호출 될 때 Service를 종료시키는 로직을 추가했습니다.
     * Service를 종료시킬 때 view들을 제거해줘야 완전히 화면에서 사라집니다.
     */
    override fun onDestroy() {
        removeViews()
        super.onDestroy()
    }

    private fun removeViews() {
        if (boxView.parent != null) windowManager.removeView(boxView)
        if (changeButton.parent != null) windowManager.removeView(changeButton)
        if (closeButton.parent != null) windowManager.removeView(closeButton)
    }

}