Coverage Summary for Class: ViewLayoutExtensionsKt (kr.open.library.simple_ui.xml.extensions.view)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ViewLayoutExtensionsKt |
100%
(9/9)
|
81%
(34/42)
|
100%
(59/59)
|
98.6%
(357/362)
|
| ViewLayoutExtensionsKt$applyWindowInsetsAsPadding$2 |
33.3%
(1/3)
|
|
25%
(1/4)
|
22.2%
(2/9)
|
| ViewLayoutExtensionsKt$doOnLayout$1 |
100%
(2/2)
|
|
100%
(3/3)
|
100%
(14/14)
|
| Total |
85.7%
(12/14)
|
81%
(34/42)
|
95.5%
(63/66)
|
96.9%
(373/385)
|
/**
* View layout and lifecycle extension functions.<br>
* Provides convenient methods for layout inflation, lifecycle observation, and window insets handling.<br><br>
* View 레이아웃 및 라이프사이클 확장 함수입니다.<br>
* 레이아웃 인플레이션, 라이프사이클 관찰 및 윈도우 인셋 처리를 위한 편리한 메서드를 제공합니다.<br>
*
* Example usage:<br>
* ```kotlin
* // Layout inflation
* val view = viewGroup.getLayoutInflater(R.layout.item_view, false)
*
* // Lifecycle observation
* view.bindLifecycleObserver(observer)
* view.unbindLifecycleObserver(observer)
*
* // Layout callbacks
* view.doOnLayout { v ->
* val width = v.width
* val height = v.height
* }
*
* // Window insets
* rootView.applyWindowInsetsAsPadding(bottom = true)
* ```
*/
package kr.open.library.simple_ui.xml.extensions.view
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import kr.open.library.simple_ui.core.extensions.trycatch.safeCatch
import kr.open.library.simple_ui.core.thread.assertMainThreadDebug
import kotlin.math.max
/**
* Inflates a layout resource into this ViewGroup.<br><br>
* 레이아웃 리소스를 이 ViewGroup에 인플레이트합니다.<br>
*
* @param xmlRes Layout resource ID.<br><br>
* 레이아웃 리소스 ID.<br>
*
* @param attachToRoot Whether to attach the inflated layout to the root.<br><br>
* 인플레이트된 레이아웃을 루트에 연결할지 여부.<br>
*
* @return The inflated View.<br><br>
* 인플레이트된 View.<br>
*/
@SuppressLint("ResourceType")
public fun ViewGroup.getLayoutInflater(
@LayoutRes xmlRes: Int,
attachToRoot: Boolean,
): View = LayoutInflater.from(this.context).inflate(xmlRes, this, attachToRoot)
/**
* Finds the host LifecycleOwner for this View.<br>
* Prioritizes Fragment's viewLifecycleOwner, falls back to Activity.<br><br>
* 이 View의 호스트 LifecycleOwner를 찾습니다.<br>
* Fragment의 viewLifecycleOwner를 우선시하고, 없으면 Activity를 사용합니다.<br>
*
* @return The LifecycleOwner or null if not found.<br><br>
* LifecycleOwner 또는 찾을 수 없으면 null.<br>
*/
@MainThread
inline fun View.findHostLifecycleOwner(): LifecycleOwner? {
assertMainThreadDebug("View.findHostLifecycleOwner")
return findViewTreeLifecycleOwner() ?: (context as? LifecycleOwner)
}
/**
* Binds a lifecycle observer to the current LifecycleOwner.<br>
* Replaces the observer if the owner changes, prevents duplicate registration.<br><br>
* 현재 LifecycleOwner에 라이프사이클 옵저버를 바인딩합니다.<br>
* Owner가 변경되면 옵저버를 교체하고, 중복 등록을 방지합니다.<br>
*
* @param observer The lifecycle observer to bind.<br><br>
* 바인딩할 라이프사이클 옵저버.<br>
*
* @return The current LifecycleOwner or null if binding failed.<br><br>
* 현재 LifecycleOwner 또는 바인딩 실패 시 null.<br>
*/
@MainThread
fun View.bindLifecycleObserver(observer: DefaultLifecycleObserver): LifecycleOwner? {
assertMainThreadDebug("View.bindLifecycleObserver")
val current = findHostLifecycleOwner() ?: return null
val bindings = getLifecycleObserverBindings()
val oldOwner = bindings[observer]
if (oldOwner !== current) {
oldOwner?.lifecycle?.removeObserver(observer)
val registered =
safeCatch(false) {
current.lifecycle.addObserver(observer)
true
}
if (!registered) return null
bindings[observer] = current
setTag(ViewIds.TAG_OBSERVED_OWNER, bindings)
}
return current
}
/**
* Unbinds the lifecycle observer from this View.<br>
* Should be called when detaching the view or changing parent.<br><br>
* 이 View에서 라이프사이클 옵저버를 언바인딩합니다.<br>
* View를 분리하거나 부모를 변경할 때 호출해야 합니다.<br>
*
* @param observer The lifecycle observer to unbind.<br><br>
* 언바인딩할 라이프사이클 옵저버.<br>
*/
@MainThread
fun View.unbindLifecycleObserver(observer: DefaultLifecycleObserver) {
assertMainThreadDebug("View.unbindLifecycleObserver")
@Suppress("UNCHECKED_CAST")
val bindings =
getTag(ViewIds.TAG_OBSERVED_OWNER) as? MutableMap<DefaultLifecycleObserver, LifecycleOwner>
?: return
bindings.remove(observer)?.lifecycle?.removeObserver(observer)
if (bindings.isEmpty()) {
setTag(ViewIds.TAG_OBSERVED_OWNER, null)
} else {
setTag(ViewIds.TAG_OBSERVED_OWNER, bindings)
}
}
@MainThread
private fun View.getLifecycleObserverBindings(): MutableMap<DefaultLifecycleObserver, LifecycleOwner> {
assertMainThreadDebug("View.getLifecycleObserverBindings")
@Suppress("UNCHECKED_CAST")
val existing =
getTag(ViewIds.TAG_OBSERVED_OWNER) as? MutableMap<DefaultLifecycleObserver, LifecycleOwner>
if (existing != null) return existing
return mutableMapOf<DefaultLifecycleObserver, LifecycleOwner>().also {
setTag(ViewIds.TAG_OBSERVED_OWNER, it)
}
}
/**
* Executes a block when the view has been laid out and measured.<br>
* Useful for getting actual view dimensions.<br><br>
* View가 레이아웃되고 측정된 후 블록을 실행합니다.<br>
* 실제 View 크기를 얻는 데 유용합니다.<br>
*
* @param action Block to execute when view is laid out.<br><br>
* View가 레이아웃될 때 실행할 블록.<br>
*/
@MainThread
public inline fun View.doOnLayout(crossinline action: (view: View) -> Unit) {
assertMainThreadDebug("View.doOnLayout")
if (isLaidOut && !isLayoutRequested) {
action(this)
} else {
viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
action(this@doOnLayout)
}
},
)
}
}
/**
* Gets the view's location on screen as a Pair.<br><br>
* View의 화면상 위치를 Pair로 가져옵니다.<br>
*
* @return Pair of (x, y) coordinates on screen.<br><br>
* 화면상 (x, y) 좌표의 Pair.<br>
*/
@MainThread
public fun View.getLocationOnScreen(): Pair<Int, Int> {
assertMainThreadDebug("View.getLocationOnScreen")
val location = IntArray(2)
getLocationOnScreen(location)
return Pair(location[0], location[1])
}
/**
* Applies window insets as padding to the view.<br>
* Useful for handling system bars and keyboard.<br><br>
* 윈도우 인셋을 View의 패딩으로 적용합니다.<br>
* 시스템 바 및 키보드 처리에 유용합니다.<br>
*
* @param left Whether to apply left inset as left padding (default: true).<br><br>
* 왼쪽 인셋을 왼쪽 패딩으로 적용할지 여부 (기본값: true).<br>
*
* @param top Whether to apply top inset as top padding (default: true).<br><br>
* 상단 인셋을 상단 패딩으로 적용할지 여부 (기본값: true).<br>
*
* @param right Whether to apply right inset as right padding (default: true).<br><br>
* 오른쪽 인셋을 오른쪽 패딩으로 적용할지 여부 (기본값: true).<br>
*
* @param bottom Whether to apply bottom inset as bottom padding (default: true).<br><br>
* 하단 인셋을 하단 패딩으로 적용할지 여부 (기본값: true).<br>
*/
@MainThread
public fun View.applyWindowInsetsAsPadding(
left: Boolean = true,
top: Boolean = true,
right: Boolean = true,
bottom: Boolean = true,
) {
assertMainThreadDebug("View.applyWindowInsetsAsPadding")
val initialPadding = Pair(Pair(paddingLeft, paddingTop), Pair(paddingRight, paddingBottom))
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val bottomInset = max(systemBars.bottom, imeInsets.bottom)
view.setPadding(
if (left) initialPadding.first.first + systemBars.left else initialPadding.first.first,
if (top) initialPadding.first.second + systemBars.top else initialPadding.first.second,
if (right) initialPadding.second.first + systemBars.right else initialPadding.second.first,
if (bottom) initialPadding.second.second + bottomInset else initialPadding.second.second,
)
insets
}
if (isAttachedToWindow) {
ViewCompat.requestApplyInsets(this)
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
removeOnAttachStateChangeListener(this)
ViewCompat.requestApplyInsets(v)
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}