434 lines
13 KiB
Kotlin
434 lines
13 KiB
Kotlin
package com.gh.common.util
|
||
|
||
import android.content.ClipboardManager
|
||
import android.content.Context
|
||
import android.text.Editable
|
||
import android.text.Html
|
||
import android.text.Spanned
|
||
import android.text.TextWatcher
|
||
import android.util.TypedValue
|
||
import android.view.Gravity
|
||
import android.view.View
|
||
import android.widget.PopupWindow
|
||
import android.widget.TextView
|
||
import androidx.annotation.ColorRes
|
||
import androidx.core.text.HtmlCompat
|
||
import androidx.fragment.app.Fragment
|
||
import androidx.fragment.app.FragmentActivity
|
||
import androidx.lifecycle.*
|
||
import androidx.recyclerview.widget.RecyclerView
|
||
import androidx.viewpager.widget.ViewPager
|
||
import com.facebook.drawee.view.SimpleDraweeView
|
||
import com.gh.common.constant.Config
|
||
import com.gh.common.constant.Constants
|
||
import com.gh.gamecenter.BuildConfig
|
||
import com.gh.gamecenter.R
|
||
import com.google.gson.reflect.TypeToken
|
||
import com.halo.assistant.HaloApp
|
||
import com.lightgame.utils.Utils
|
||
import okhttp3.MediaType
|
||
import okhttp3.RequestBody
|
||
import java.net.URI
|
||
import kotlin.math.abs
|
||
|
||
/**
|
||
* 创建以 activity 为观察者上下文的 viewModel
|
||
*/
|
||
inline fun <reified VM : ViewModel> FragmentActivity.viewModelProvider(
|
||
provider: ViewModelProvider.Factory? = null
|
||
) =
|
||
ViewModelProviders.of(this, provider).get(VM::class.java)
|
||
|
||
/**
|
||
* 创建以 activity 为观察者上下文的 viewModel
|
||
*/
|
||
inline fun <reified VM : ViewModel> Fragment.viewModelProviderFromParent(
|
||
provider: ViewModelProvider.Factory? = null
|
||
) =
|
||
ViewModelProviders.of(requireActivity(), provider).get(VM::class.java)
|
||
|
||
/**
|
||
* 创建以 fragment 为观察者上下文的 viewModel
|
||
*/
|
||
inline fun <reified VM : ViewModel> Fragment.viewModelProvider(
|
||
provider: ViewModelProvider.Factory? = null
|
||
) =
|
||
ViewModelProviders.of(this, provider).get(VM::class.java)
|
||
|
||
/**
|
||
*
|
||
* ViewPager Extensions
|
||
*
|
||
*/
|
||
fun ViewPager.doOnPageSelected(action: (position: Int) -> Unit) = addOnPageChangeListener(onSelected = action)
|
||
|
||
fun ViewPager.addOnPageChangeListener(onSelected: ((position: Int) -> Unit)? = null) {
|
||
val listener = object : ViewPager.OnPageChangeListener {
|
||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||
// Do nothing.
|
||
}
|
||
|
||
override fun onPageSelected(position: Int) {
|
||
onSelected?.invoke(position)
|
||
}
|
||
|
||
override fun onPageScrollStateChanged(state: Int) {
|
||
// Do nothing.
|
||
}
|
||
}
|
||
addOnPageChangeListener(listener)
|
||
}
|
||
|
||
fun ViewPager.addOnScrollStateChanged(onStateChanged: ((state: Int) -> Unit)? = null) {
|
||
val listener = object : ViewPager.OnPageChangeListener {
|
||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||
}
|
||
|
||
override fun onPageSelected(position: Int) {
|
||
}
|
||
|
||
override fun onPageScrollStateChanged(state: Int) {
|
||
onStateChanged?.invoke(state)
|
||
}
|
||
}
|
||
addOnPageChangeListener(listener)
|
||
}
|
||
|
||
/**
|
||
* RecyclerView Extensions
|
||
*/
|
||
|
||
// 监听滚动距离
|
||
fun RecyclerView.doOnScrolledSpecificDistance(distanceX: Int = 0, distanceY: Int = 0, singleTimeEvent: Boolean = false, action: () -> Unit) {
|
||
val listener = object : RecyclerView.OnScrollListener() {
|
||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||
super.onScrolled(recyclerView, dx, dy)
|
||
|
||
if ((distanceX != 0 && abs(dx) > distanceX) || (distanceY != 0 && abs(dy) > distanceY)) {
|
||
action.invoke()
|
||
if (singleTimeEvent) {
|
||
removeOnScrollListener(this)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
addOnScrollListener(listener)
|
||
}
|
||
|
||
/**
|
||
* View Extensions
|
||
*/
|
||
fun View.visibleIf(predicate: Boolean) {
|
||
visibility = if (predicate) {
|
||
View.VISIBLE
|
||
} else {
|
||
View.INVISIBLE
|
||
}
|
||
}
|
||
|
||
fun View.goneIf(predicate: Boolean) {
|
||
visibility = if (predicate) {
|
||
View.GONE
|
||
} else {
|
||
View.VISIBLE
|
||
}
|
||
}
|
||
|
||
fun View.addSelectableItemBackground() {
|
||
val outValue = TypedValue()
|
||
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
|
||
setBackgroundResource(outValue.resourceId);
|
||
}
|
||
|
||
fun View.removeSelectableItemBackground() {
|
||
background = null
|
||
}
|
||
|
||
/**
|
||
* LiveData Extensions
|
||
*/
|
||
fun <T> LiveData<T?>.observeNonNull(owner: LifecycleOwner, callback: (T) -> Unit) {
|
||
observe(owner, Observer { value ->
|
||
if (value != null) {
|
||
callback(value)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Login related extensions
|
||
*/
|
||
fun Fragment.ifLogin(entrance: String, action: (() -> Unit)? = null) {
|
||
requireContext().ifLogin(entrance, action)
|
||
}
|
||
|
||
fun Context.ifLogin(entrance: String, action: (() -> Unit)? = null) {
|
||
CheckLoginUtils.checkLogin(this, entrance, action)
|
||
}
|
||
|
||
|
||
/**
|
||
* Gson related extensions.
|
||
*/
|
||
inline fun <reified T : Any> String.toObject(): T? {
|
||
return try {
|
||
GsonUtils.gson.fromJson(this, object : TypeToken<T>() {}.type)
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
null
|
||
}
|
||
}
|
||
|
||
inline fun <reified T : Any> T.toJson(): String {
|
||
return GsonUtils.toJson(this)
|
||
}
|
||
|
||
/**
|
||
* 在限定 interval 里只触发一次 action
|
||
*/
|
||
fun debounceActionWithInterval(id: Int, interval: Long = 300, action: (() -> Unit)? = null) {
|
||
if (!ClickUtils.isFastDoubleClick(id, interval)) {
|
||
action?.invoke()
|
||
}
|
||
}
|
||
|
||
fun View.debounceActionWithInterval(interval: Long = 300, action: (() -> Unit)? = null) {
|
||
debounceActionWithInterval(this.id, interval, action)
|
||
}
|
||
|
||
/**
|
||
* 告诉需要返回 true or false 的外层这个事件已经被消费(即返回 true)
|
||
*/
|
||
inline fun consume(f: () -> Unit): Boolean {
|
||
f()
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 简化那些不得不写 try catch 的代码块
|
||
*/
|
||
inline fun tryWithDefaultCatch(action: (() -> Unit)) {
|
||
try {
|
||
action.invoke()
|
||
} catch (e: Throwable) {
|
||
e.printStackTrace()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* String related
|
||
*/
|
||
fun String.fromHtml(): Spanned {
|
||
return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||
}
|
||
|
||
// 去掉文章/答案的插入内容
|
||
fun String.removeInsertedContent(): String {
|
||
val textRegex = "(?s)<div class=\"gh-internal-content content-right\".*?</div>"
|
||
return this.replace(textRegex.toRegex(), "")
|
||
}
|
||
|
||
// 去除视频相关文本
|
||
fun String.removeVideoContent(): String {
|
||
val videoRegex = "(?s)<div class=\"insert-video-container\".*?</div>"
|
||
return this.replace(videoRegex.toRegex(), "")
|
||
}
|
||
|
||
// 完全地清除所有 Html 格式
|
||
fun String.clearHtmlFormatCompletely(): String {
|
||
return Html.fromHtml(this).toString().replace('\n', 32.toChar())
|
||
.replace(160.toChar(), 32.toChar()).replace(65532.toChar(), 32.toChar()).trim { it <= ' ' }
|
||
}
|
||
|
||
// 如果该字符串长度超过固定长度的话,从头开始截取固定长度并返回
|
||
fun String.subStringIfPossible(length: Int): String {
|
||
return if (this.length > length) {
|
||
this.substring(0, length)
|
||
} else {
|
||
this
|
||
}
|
||
}
|
||
|
||
fun String.copyTextAndToast(toastText: String = "复制成功") {
|
||
val application = HaloApp.getInstance().application
|
||
val cmb = application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||
cmb.text = this
|
||
Utils.toast(application, toastText)
|
||
}
|
||
|
||
fun Map<String, String>.createRequestBody(): RequestBody {
|
||
val json = GsonUtils.toJson(this)
|
||
return RequestBody.create(MediaType.parse("application/json"), json)
|
||
}
|
||
|
||
// 对在浏览器(WebView)显示的路径进行转码
|
||
fun String.decodeURI(): String {
|
||
return URI(null, null, this, null).rawPath
|
||
}
|
||
|
||
/**
|
||
* 根据手机的分辨率从 dip(像素) 的单位 转成为 px
|
||
*/
|
||
fun Float.dip2px(): Int {
|
||
val scale = HaloApp.getInstance().application.resources.displayMetrics.density
|
||
return (this * scale + 0.5f).toInt()
|
||
}
|
||
|
||
/**
|
||
* 根据手机的分辨率从 px(像素) 的单位 转成为 dip
|
||
*/
|
||
fun Float.px2dip(): Int {
|
||
val scale = HaloApp.getInstance().application.resources.displayMetrics.density
|
||
return (this / scale + 0.5f).toInt()
|
||
}
|
||
|
||
/**
|
||
* PopupWindow 自动适配方向
|
||
*/
|
||
fun PopupWindow.showAutoOrientation(anchorView: View, distanceY: Int = 0) {
|
||
val windowPos = IntArray(2)
|
||
val anchorLoc = IntArray(2)
|
||
// 获取锚点View在屏幕上的左上角坐标位置
|
||
anchorView.getLocationOnScreen(anchorLoc)
|
||
val anchorHeight = anchorView.height + distanceY
|
||
// 获取屏幕的高宽
|
||
val screenHeight = anchorView.context.resources.displayMetrics.heightPixels
|
||
val screenWidth = anchorView.context.resources.displayMetrics.widthPixels
|
||
// 测量contentView
|
||
contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||
// 计算contentView的高宽
|
||
val windowHeight = contentView.measuredHeight
|
||
val windowWidth = contentView.measuredWidth
|
||
// 判断需要向上弹出还是向下弹出显示
|
||
val isNeedShowUp = screenHeight - anchorLoc[1] - anchorHeight < windowHeight
|
||
if (isNeedShowUp) {
|
||
windowPos[0] = screenWidth - windowWidth
|
||
windowPos[1] = anchorLoc[1] - windowHeight
|
||
} else {
|
||
windowPos[0] = screenWidth - windowWidth
|
||
windowPos[1] = anchorLoc[1] + anchorHeight
|
||
}
|
||
|
||
animationStyle = R.style.popwindow_option_anim_style
|
||
showAtLocation(anchorView, Gravity.TOP or Gravity.START, windowPos[0], windowPos[1])
|
||
}
|
||
|
||
/**
|
||
* 权限相关
|
||
*/
|
||
fun Fragment.checkReadPhoneStateAndStoragePermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkReadPhoneStateAndStoragePermissionBeforeAction(requireContext(), object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun Fragment.checkReadPhoneStatePermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkReadPhoneStatePermissionBeforeAction(requireContext(), object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun Fragment.checkStoragePermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkStoragePermissionBeforeAction(requireContext(), object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun FragmentActivity.checkReadPhoneStateAndStoragePermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkReadPhoneStateAndStoragePermissionBeforeAction(this, object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun FragmentActivity.checkReadPhoneStatePermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkReadPhoneStatePermissionBeforeAction(this, object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun FragmentActivity.checkStoragePermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkStoragePermissionBeforeAction(this, object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* TextView related.
|
||
*/
|
||
fun TextView.setTextWithHighlightedTextWrappedInsideWrapper(text: CharSequence,
|
||
wrapper: String = Constants.DEFAULT_TEXT_WRAPPER,
|
||
@ColorRes
|
||
highlightColorId: Int = R.color.theme,
|
||
copyClickedText: Boolean = false,
|
||
highlightedTextClickListener: (() -> Unit)? = null) {
|
||
TextHelper.highlightTextThatIsWrappedInsideWrapper(this, text, wrapper, highlightColorId, object : SimpleCallback<String> {
|
||
override fun onCallback(arg: String) {
|
||
if (copyClickedText) {
|
||
arg.copyTextAndToast("已复制:$arg")
|
||
}
|
||
highlightedTextClickListener?.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun TextView.setTextChangedListener(action: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit) {
|
||
this.addTextChangedListener(object : TextWatcher {
|
||
override fun afterTextChanged(s: Editable?) {
|
||
|
||
}
|
||
|
||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||
}
|
||
|
||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||
action.invoke(s ?: "", start, before, count)
|
||
}
|
||
})
|
||
}
|
||
|
||
fun Int.toColor(): Int {
|
||
return HaloApp.getInstance().application.resources.getColor(this)
|
||
}
|
||
|
||
fun Int.toResString(): String {
|
||
return HaloApp.getInstance().application.resources.getString(this)
|
||
}
|
||
|
||
fun Int.toSimpleCount(): String {
|
||
return NumberUtils.transSimpleCount(this)
|
||
}
|
||
|
||
/**
|
||
* Image related
|
||
*/
|
||
fun SimpleDraweeView.display(url: String) {
|
||
ImageUtils.display(this, url)
|
||
}
|
||
|
||
/**
|
||
* 测试用包裹
|
||
*/
|
||
inline fun debugOnly(f: () -> Unit) {
|
||
if (BuildConfig.DEBUG) {
|
||
f()
|
||
}
|
||
}
|
||
|
||
inline fun testChannelOnly(f: () -> Unit) {
|
||
if (HaloApp.getInstance().channel == Config.DEFAULT_CHANNEL) {
|
||
f()
|
||
}
|
||
} |