ViewExtensions.kt 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. @file:Suppress("unused")
  2. package io.legato.kazusa.utils
  3. import android.annotation.SuppressLint
  4. import android.content.Context
  5. import android.graphics.Bitmap
  6. import android.graphics.Canvas
  7. import android.graphics.Color
  8. import android.graphics.Picture
  9. import android.os.Build
  10. import android.text.Html
  11. import android.view.MotionEvent
  12. import android.view.View
  13. import android.view.View.GONE
  14. import android.view.View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
  15. import android.view.View.INVISIBLE
  16. import android.view.View.VISIBLE
  17. import android.view.ViewGroup
  18. import android.view.inputmethod.InputMethodManager
  19. import android.widget.EdgeEffect
  20. import android.widget.EditText
  21. import android.widget.RadioGroup
  22. import android.widget.SeekBar
  23. import android.widget.TextView
  24. import androidx.annotation.ColorInt
  25. import androidx.annotation.DrawableRes
  26. import androidx.appcompat.app.AppCompatActivity
  27. import androidx.appcompat.view.menu.MenuPopupHelper
  28. import androidx.appcompat.widget.PopupMenu
  29. import androidx.core.graphics.createBitmap
  30. import androidx.core.graphics.record
  31. import androidx.core.graphics.withTranslation
  32. import androidx.core.view.ViewCompat
  33. import androidx.core.view.WindowInsetsCompat
  34. import androidx.core.view.get
  35. import androidx.core.view.isVisible
  36. import androidx.core.view.marginBottom
  37. import androidx.core.view.updateLayoutParams
  38. import androidx.recyclerview.widget.RecyclerView
  39. import androidx.viewpager.widget.ViewPager
  40. import io.legato.kazusa.help.config.AppConfig
  41. import io.legato.kazusa.lib.theme.TintHelper
  42. import io.legato.kazusa.utils.canvasrecorder.CanvasRecorder
  43. import io.legato.kazusa.utils.canvasrecorder.record
  44. import splitties.systemservices.inputMethodManager
  45. import splitties.views.bottomPadding
  46. import splitties.views.topPadding
  47. import java.lang.reflect.Field
  48. private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? {
  49. return when (context) {
  50. is AppCompatActivity -> context
  51. is androidx.appcompat.view.ContextThemeWrapper -> getCompatActivity(context.baseContext)
  52. is android.view.ContextThemeWrapper -> getCompatActivity(context.baseContext)
  53. else -> null
  54. }
  55. }
  56. val View.activity: AppCompatActivity?
  57. get() = getCompatActivity(context)
  58. fun View.hideSoftInput() = run {
  59. inputMethodManager.hideSoftInputFromWindow(this.windowToken, 0)
  60. }
  61. fun EditText.showSoftInput() = run {
  62. requestFocus()
  63. inputMethodManager.showSoftInput(this, InputMethodManager.RESULT_SHOWN)
  64. }
  65. fun View.disableAutoFill() = run {
  66. this.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
  67. }
  68. fun View.applyTint(
  69. @ColorInt color: Int,
  70. isDark: Boolean = AppConfig.isNightTheme
  71. ) {
  72. TintHelper.setTintAuto(this, color, false, isDark)
  73. }
  74. fun View.applyBackgroundTint(
  75. @ColorInt color: Int,
  76. isDark: Boolean = AppConfig.isNightTheme
  77. ) {
  78. if (background == null) {
  79. setBackgroundColor(color)
  80. } else {
  81. TintHelper.setTintAuto(this, color, true, isDark)
  82. }
  83. }
  84. fun RecyclerView.setEdgeEffectColor(@ColorInt color: Int) {
  85. edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() {
  86. override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
  87. val edgeEffect = super.createEdgeEffect(view, direction)
  88. edgeEffect.color = color
  89. return edgeEffect
  90. }
  91. }
  92. }
  93. fun ViewPager.setEdgeEffectColor(@ColorInt color: Int) {
  94. try {
  95. val clazz = ViewPager::class.java
  96. for (name in arrayOf("mLeftEdge", "mRightEdge")) {
  97. val field = clazz.getDeclaredField(name)
  98. field.isAccessible = true
  99. val edge = field.get(this)
  100. (edge as EdgeEffect).color = color
  101. }
  102. } catch (ignored: Exception) {
  103. }
  104. }
  105. fun EditText.disableEdit() {
  106. keyListener = null
  107. }
  108. fun View.gone() {
  109. if (visibility != GONE) {
  110. visibility = GONE
  111. }
  112. }
  113. fun View.gone(gone: Boolean) {
  114. if (gone) {
  115. gone()
  116. } else {
  117. visibility = VISIBLE
  118. }
  119. }
  120. fun View.invisible() {
  121. if (visibility != INVISIBLE) {
  122. visibility = INVISIBLE
  123. }
  124. }
  125. fun View.visible() {
  126. if (visibility != VISIBLE) {
  127. visibility = VISIBLE
  128. }
  129. }
  130. fun View.visible(visible: Boolean) {
  131. if (visible && visibility != VISIBLE) {
  132. visibility = VISIBLE
  133. } else if (!visible && isVisible) {
  134. visibility = INVISIBLE
  135. }
  136. }
  137. fun View.screenshot(bitmap: Bitmap? = null, canvas: Canvas? = null): Bitmap? {
  138. return if (width > 0 && height > 0) {
  139. val screenshot = if (bitmap != null && bitmap.width == width && bitmap.height == height) {
  140. bitmap.eraseColor(Color.TRANSPARENT)
  141. bitmap
  142. } else {
  143. bitmap?.recycle()
  144. createBitmap(width, height)
  145. }
  146. val c = canvas ?: Canvas()
  147. c.setBitmap(screenshot)
  148. c.save()
  149. c.translate(-scrollX.toFloat(), -scrollY.toFloat())
  150. this.draw(c)
  151. c.restore()
  152. c.setBitmap(null)
  153. screenshot.prepareToDraw()
  154. screenshot
  155. } else {
  156. null
  157. }
  158. }
  159. fun View.screenshot(picture: Picture) {
  160. if (width > 0 && height > 0) {
  161. picture.record(width, height) {
  162. withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) {
  163. draw(this)
  164. }
  165. }
  166. }
  167. }
  168. fun View.screenshot(canvasRecorder: CanvasRecorder) {
  169. if (width > 0 && height > 0) {
  170. canvasRecorder.record(width, height) {
  171. draw(this)
  172. }
  173. }
  174. }
  175. fun View.setPaddingBottom(bottom: Int) {
  176. setPadding(paddingLeft, paddingTop, paddingRight, bottom)
  177. }
  178. fun SeekBar.progressAdd(int: Int) {
  179. progress += int
  180. }
  181. fun RadioGroup.getIndexById(id: Int): Int {
  182. for (i in 0 until this.childCount) {
  183. if (id == get(i).id) {
  184. return i
  185. }
  186. }
  187. return 0
  188. }
  189. fun RadioGroup.getCheckedIndex(): Int {
  190. for (i in 0 until this.childCount) {
  191. if (checkedRadioButtonId == get(i).id) {
  192. return i
  193. }
  194. }
  195. return 0
  196. }
  197. fun RadioGroup.checkByIndex(index: Int) {
  198. check(get(index).id)
  199. }
  200. @SuppressLint("ObsoleteSdkInt")
  201. fun TextView.setHtml(html: String) {
  202. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  203. text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT)
  204. } else {
  205. @Suppress("DEPRECATION")
  206. text = Html.fromHtml(html)
  207. }
  208. }
  209. fun TextView.setTextIfNotEqual(charSequence: CharSequence?) {
  210. if (text != charSequence) {
  211. text = charSequence
  212. }
  213. }
  214. @SuppressLint("RestrictedApi")
  215. fun PopupMenu.show(x: Int, y: Int) {
  216. kotlin.runCatching {
  217. val field: Field = this.javaClass.getDeclaredField("mPopup")
  218. field.isAccessible = true
  219. (field.get(this) as MenuPopupHelper).show(x, y)
  220. }.onFailure {
  221. it.printOnDebug()
  222. }
  223. }
  224. fun View.shouldHideSoftInput(event: MotionEvent): Boolean {
  225. if (this is EditText) {
  226. val l = intArrayOf(0, 0)
  227. getLocationInWindow(l)
  228. val left = l[0]
  229. val top = l[1]
  230. val bottom = top + getHeight()
  231. val right = left + getWidth()
  232. return !(event.x > left && event.x < right && event.y > top && event.y < bottom)
  233. }
  234. return false
  235. }
  236. fun View.applyStatusBarPadding(withInitialPadding: Boolean = false) {
  237. val initialPadding = if (withInitialPadding) topPadding else 0
  238. setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
  239. val insets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
  240. topPadding = initialPadding + insets.top
  241. windowInsets
  242. }
  243. }
  244. fun View.applyNavigationBarPadding(withInitialPadding: Boolean = false) {
  245. val initialPadding = if (withInitialPadding) bottomPadding else 0
  246. setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
  247. bottomPadding = initialPadding + windowInsets.navigationBarHeight
  248. windowInsets
  249. }
  250. }
  251. fun View.applyNavigationBarMargin(withInitialMargin: Boolean = false) {
  252. val initialMargin = if (withInitialMargin) marginBottom else 0
  253. setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
  254. updateLayoutParams<ViewGroup.MarginLayoutParams> {
  255. bottomMargin = initialMargin + windowInsets.navigationBarHeight
  256. }
  257. windowInsets
  258. }
  259. }
  260. fun View.setBackgroundKeepPadding(@DrawableRes backgroundResId: Int) {
  261. val paddingLeft = paddingLeft
  262. val paddingTop = paddingTop
  263. val paddingRight = paddingRight
  264. val paddingBottom = paddingBottom
  265. setBackgroundResource(backgroundResId)
  266. setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
  267. }
  268. fun View.canScroll(direction: Int): Boolean {
  269. return canScrollVertically(direction) || canScrollHorizontally(direction)
  270. }
  271. private val requestLayoutBroken =
  272. Build.VERSION.SDK_INT in Build.VERSION_CODES.O..Build.VERSION_CODES.Q
  273. fun View.setOnApplyWindowInsetsListenerCompat(listener: (View, WindowInsetsCompat) -> WindowInsetsCompat) {
  274. ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
  275. val windowInsets = listener(view, insets)
  276. if (requestLayoutBroken && isLayoutRequested) {
  277. post {
  278. requestLayout()
  279. }
  280. }
  281. windowInsets
  282. }
  283. }