안드로이드/기능구현

웹뷰 PIP 자체 구현해보기 [ AOS 기능구현 ]

결과

 

앱 내부에서 동작하는 PIP를 구현해보았습니다.

 

제가 설계한 구성은 다음과 같습니다.

MainActivity

- ImageView(말티즈 이미지)

- FragmentContainerView(웹뷰 표시할 FragmentContainer)

 

WebviewLiveFragment

- WebView(영상 스트리밍 웹뷰) *그냥 일반 웹뷰를 사용하셔도 무방합니다.

- LinearLayout(PIP 제어 버튼)

-- Button(Full) - 풀모드로 전환

-- Button(PIP) - PIP모드로 전환

-- Button(X) - (웹뷰 reload 기능으로 설정)

 

우선 저는 PIP모드를 ViewModel로 관리하기로 했습니다.

class WvpipViewModel: ViewModel() {
    val pip: MutableLiveData<Boolean> = MutableLiveData<Boolean>()

    fun setPip(bool: Boolean) {
        pip.value = bool
    }
}

pip를 setting하며 true, false로 값을 관리합니다.

 

MainActivty

private lateinit var binding: ActivityMainBinding

// pip view model
private val pipViewModel: WvpipViewModel by viewModels()

// pip init size
private var pipWidth = 200
private var pipHeight = 300
private var pipX = 0f
private var pipY = 0f


// statusBar size
private var statusBarSize = 0

gradle에서 viewBinding을 true로 하여 binding했습니다.

선언부에 viewModel을 선언하고 pip의 기본 사이즈를 세팅해줬습니다.

 

fun initValue() {
        pipX = binding.wvliveFrag.x
        pipY = binding.wvliveFrag.y

        // 상태바 사이즈
        val resId = resources.getIdentifier("status_bar_height", "dimen", "android")
        statusBarSize = resources.getDimension(resId).toInt()
    }

initValue에선 pipX, pipY값을 초기화 해주며 상태바 사이즈를 계산하는 로직이 있습니다.

상태바 사이즈를 계산하는 이유는 후에 상태바 사이즈를 제외하여 계산을 해야하기 때문입니다.

 

fun initObserve() {

        // pipViewModel의 PIP값 observe
        pipViewModel.pip.observe(this) {

            if (it == true) {
                // pip
                Log.d("테스트", "pip on")

                binding.wvliveFrag.clipToOutline = true

                wvliveFragInitPosition(it)

                val view = binding.wvliveFrag as View
                val location = intArrayOf(0,0)

                view.run {
                    doOnLayout {
                        getLocationOnScreen(location)
                        pipX = location[0].toFloat()
                        pipY = location[1].toFloat() - statusBarSize
                    }
                }
            } else {
                // full
                Log.d("테스트", "pip off" )
                wvliveFragInitPosition(it)
            }

        }
    }

 

initObserve에선 viewModel을 observe하며 pip가 true/false로 전환될 때 작업을 처리해줍니다.

작업을 처리할 때 wvliveFragInitPosition 함수를 호출하여 FragmentContainer의 위치를 잡아줍니다.

pip true일 때 좌표값을 계산해서 pipX, pipY를 갱신합니다.

 

fun wvliveFragInitPosition(pip: Boolean) {
        if (pip == true) {
            // pip 초기 위치 기억
            pipX = binding.wvliveFrag.x
            pipY = binding.wvliveFrag.y

            var layoutParam = ConstraintLayout.LayoutParams(dpToPx(this, 160f), dpToPx(this, 240f))
            layoutParam.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
            layoutParam.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
            layoutParam.bottomMargin = dpToPx(this, 88f)
            layoutParam.marginEnd = dpToPx(this, 32f)
            binding.wvliveFrag.layoutParams = layoutParam
            binding.wvliveFrag.requestLayout()

            binding.wvliveFrag.clipToOutline = true
        }
        else {
            var layoutParam = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT)
            layoutParam.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
            layoutParam.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
            layoutParam.bottomMargin = 0
            layoutParam.marginEnd = 0
            (binding.wvliveFrag as View).x = pipX
            (binding.wvliveFrag as View).y = pipY
            binding.wvliveFrag.layoutParams = layoutParam
            binding.wvliveFrag.requestLayout()

            binding.wvliveFrag.clipToOutline = false
        }

    }

 

---

 

WebviewLiveFragment

class WebviewLiveFragment : Fragment() {

    lateinit var fBinding: WebviewLiveFragmentBinding

    private val viewModel: WvpipViewModel by activityViewModels()

    // 이동량
    private var moveX = 0f
    private var moveY = 0f

선언부에는 binding과 viewModel 그리고 pip 드래그했을 때의 이동량을 초기화 했습니다.

 

onViewCreated에서 웹뷰를 세팅했으며 각 이벤트를 정의했습니다.

@SuppressLint("ClickableViewAccessibility")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        with(fBinding.wvfragWebview) {
            settings.javaScriptEnabled = true
            settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
            settings.loadWithOverviewMode = true
            settings.useWideViewPort = true
            settings.setSupportZoom(true)
            settings.builtInZoomControls = true
            // https://www.showpinglive.com/display/displayMain.do
            // Live 에서 송출 중인 Live 링크 넣기
            this.loadUrl("https://www.showpinglive.com/display/viewer.do?brodNo=202207200014&prev=m_live")
        }

        // pip 버튼 눌렀을 때
        fBinding.wvfragBtnPip.setOnClickListener {
            Log.d("테스트", "pip ON !!!")
            viewModel.setPip(true) // pip 상태 true

            setBtnSize(fBinding.wvfragBtnPip)
            setBtnSize(fBinding.wvfragBtnFull)
            setBtnSize(fBinding.wvfragBtnX)
        }

        // full 버튼 눌렀을 때
        fBinding.wvfragBtnFull.setOnClickListener {
            Log.d("테스트", "pip OFF !!!")
            viewModel.setPip(false) // pip 상태 false

            setBtnSize(fBinding.wvfragBtnPip)
            setBtnSize(fBinding.wvfragBtnFull)
            setBtnSize(fBinding.wvfragBtnX)
        }

        // X 버튼 눌렀을 때
        fBinding.wvfragBtnX.setOnClickListener {

            fBinding.wvfragWebview.reload()
        }

        // 웹뷰 pip drag move
        fBinding.wvfragWebview.setOnTouchListener { v, event ->
            if (viewModel.pip.value!! == false) {
                return@setOnTouchListener false
            } else {
                val viewParent = fBinding.root.parent as View

                when(event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        moveX = viewParent.x - event.rawX
                        moveY = viewParent.y - event.rawY
                        true
                    }
                    MotionEvent.ACTION_MOVE -> {
                        viewParent.animate()
                            .x(event.rawX + moveX)
                            .y(event.rawY + moveY)
                            .setDuration(0)
                            .start()
                        true
                    }
                    MotionEvent.ACTION_UP -> {
                        true
                    }
                    else -> false

                }
            }
        }


    }

pip drag move 이벤트에는 웹뷰를 품고 있는 FragContainerView를 대상으로 움직이게끔 처리했습니다.

이렇게 처리했을 때 앱이 실행되어있을 때 pip로 전환하면 FragContainerView의 움직임을 제어할 수 있게 됩니다.

 

fun setBtnSize(btn: ImageView) {
        var layoutParam = btn.layoutParams

        if (viewModel.pip.value == false) {
            // full
            layoutParam.width = dpToPx(requireActivity(),60f)
            layoutParam.height = dpToPx(requireActivity(),60f)

            fBinding.wvfragBtnPip.visibility = View.VISIBLE
            fBinding.wvfragBtnFull.visibility = View.GONE
        } else {
            // pip
            layoutParam.width = dpToPx(requireActivity(),30f)
            layoutParam.height = dpToPx(requireActivity(),30f)

            fBinding.wvfragBtnPip.visibility = View.GONE
            fBinding.wvfragBtnFull.visibility = View.VISIBLE
        }

        btn.layoutParams = layoutParam
    }

pip true/false되었을 때 버튼 사이즈를 갱신하는 로직입니다.

 

감사합니다.