@file:Suppress("unused")
package com.example.ext
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.GradientDrawable
import android.os.SystemClock
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.button.MaterialButton
/* ----------------------------- 가시성/상태 ----------------------------- */
/** VISIBLE/GONE 토글 */
var View.isVisibleEx: Boolean
get() = visibility == View.VISIBLE
set(v) { visibility = if (v) View.VISIBLE else View.GONE }
fun View.show() { isVisibleEx = true }
fun View.gone() { isVisibleEx = false }
fun View.invisible() { visibility = View.INVISIBLE }
/** enabled 변경과 알파를 함께 (회색 처리) */
fun View.enableWithAlpha(enabled: Boolean, disabledAlpha: Float = 0.5f) {
isEnabled = enabled
alpha = if (enabled) 1f else disabledAlpha
}
/* ----------------------------- 클릭 디바운스/스로틀 ----------------------------- */
/** 중복 탭 방지(디바운스) – intervalMs 내 추가 탭 무시 */
fun View.setOnSafeClick(intervalMs: Long = 500L, block: (View) -> Unit) {
var last = 0L
setOnClickListener {
val now = SystemClock.uptimeMillis()
if (now - last >= intervalMs) { last = now; block(it) }
}
}
/** 스로틀(첫 탭만 통과, 윈도우 내 추가 탭 무시) */
fun View.setOnThrottleClick(windowMs: Long = 800L, block: (View) -> Unit) {
var allowAt = 0L
setOnClickListener {
val now = SystemClock.uptimeMillis()
if (now >= allowAt) {
allowAt = now + windowMs
block(it)
}
}
}
/* ----------------------------- 키보드 ----------------------------- */
fun View.showKeyboard() {
requestFocus()
val imm = context.getSystemService(InputMethodManager::class.java)
imm?.showSoftInput(this, 0)
}
fun View.hideKeyboard() {
val imm = context.getSystemService(InputMethodManager::class.java)
imm?.hideSoftInputFromWindow(windowToken, 0)
}
/* ----------------------------- 인셋/레이아웃 ----------------------------- */
/** 시스템 바 인셋을 패딩에 반영 */
fun View.applySystemBarInsets(
left: Boolean = false, top: Boolean = true, right: Boolean = false, bottom: Boolean = true
) {
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updatePadding(
left = if (left) bars.left else v.paddingLeft,
top = if (top) bars.top else v.paddingTop,
right = if (right) bars.right else v.paddingRight,
bottom = if (bottom) bars.bottom else v.paddingBottom
)
WindowInsetsCompat.CONSUMED
}
}
/** 마진 수정 헬퍼 */
fun View.updateMargins(@Px left: Int? = null, @Px top: Int? = null, @Px right: Int? = null, @Px bottom: Int? = null) =
updateLayoutParams<ViewGroup.MarginLayoutParams> {
setMargins(left ?: leftMargin, top ?: topMargin, right ?: rightMargin, bottom ?: bottomMargin)
}
/** 라운드 배경 즉석 생성 */
fun View.roundBackground(@Px radius: Int, @ColorInt color: Int) {
background = GradientDrawable().apply {
cornerRadius = radius.toFloat()
setColor(color)
}
}
/** 화면에 실제로 보이는지 (겹침/크기 반영) */
fun View.isVisibleOnScreen(): Boolean {
if (!isShown) return false
val rect = Rect()
return getGlobalVisibleRect(rect) && rect.height() > 0 && rect.width() > 0
}
/* ----------------------------- 버튼: 로딩/아이콘/눌림효과 ----------------------------- */
/**
* Button/MaterialButton 공용 로딩 토글 (텍스트 옆에 인디케이터)
* - CircularProgressDrawable 사용 (권장 의존: androidx.swiperefreshlayout)
* - 토글 시 기존 텍스트/아이콘 유지
*/
private const val TAG_PROGRESS_DRAWABLE = 0x7F010001
private const val TAG_SAVED_TEXT = 0x7F010002
fun Button.setLoading(loading: Boolean, @ColorInt spinnerColor: Int? = null) {
if (loading) {
if (getTag(TAG_PROGRESS_DRAWABLE) != null) return
setTag(TAG_SAVED_TEXT, text)
enableWithAlpha(false)
val spinner = CircularProgressDrawable(context).apply {
strokeWidth = 3f
centerRadius = 9f
spinnerColor?.let { setColorSchemeColors(it) }
start()
}
// TextView 계열: compound drawable로 표시 (start 위치)
spinner.setBounds(0, 0, spinner.intrinsicWidth, spinner.intrinsicHeight)
setCompoundDrawablesRelative(spinner, null, null, null)
compoundDrawablePadding = (compoundDrawablePadding.takeIf { it > 0 } ?: 16)
setTag(TAG_PROGRESS_DRAWABLE, spinner)
} else {
(getTag(TAG_PROGRESS_DRAWABLE) as? CircularProgressDrawable)?.stop()
setCompoundDrawablesRelative(null, null, null, null)
enableWithAlpha(true)
(getTag(TAG_SAVED_TEXT) as? CharSequence)?.let { text = it }
setTag(TAG_PROGRESS_DRAWABLE, null)
setTag(TAG_SAVED_TEXT, null)
}
}
/** MaterialButton: 아이콘 오른쪽/왼쪽 여백 간단 조절 */
fun MaterialButton.iconPaddingHorizontal(@Px padding: Int) {
iconPadding = padding
insetLeft = insetLeft // no-op but keeps clarity
insetRight = insetRight
}
/** 눌림(scale) 애니메이션 – 짧고 기분 좋은 피드백 */
fun View.enablePressedScale(enabled: Boolean = true, scale: Float = 0.98f) {
if (!enabled) {
setOnTouchListener(null)
scaleX = 1f; scaleY = 1f
return
}
setOnTouchListener { v, ev ->
when (ev.actionMasked) {
android.view.MotionEvent.ACTION_DOWN -> { v.animate().scaleX(scale).scaleY(scale).setDuration(80).start() }
android.view.MotionEvent.ACTION_UP,
android.view.MotionEvent.ACTION_CANCEL -> { v.animate().scaleX(1f).scaleY(1f).setDuration(80).start() }
}
false
}
}