앱 내부에서 동작하는 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되었을 때 버튼 사이즈를 갱신하는 로직입니다.
감사합니다.
'안드로이드 > 기능구현' 카테고리의 다른 글
Kotlin invoke의 개념과 사용법 그리고 예시 (0) | 2022.09.26 |
---|---|
커스텀다이얼로그 + RadioButton with Kotlin (0) | 2022.08.12 |