950 lines
29 KiB
Kotlin
950 lines
29 KiB
Kotlin
package com.gh.common.util
|
||
|
||
import android.animation.Animator
|
||
import android.content.ClipData
|
||
import android.content.ClipboardManager
|
||
import android.content.Context
|
||
import android.graphics.drawable.GradientDrawable
|
||
import android.os.Build
|
||
import android.text.*
|
||
import android.text.style.ClickableSpan
|
||
import android.text.style.ImageSpan
|
||
import android.text.style.URLSpan
|
||
import android.util.TypedValue
|
||
import android.view.Gravity
|
||
import android.view.View
|
||
import android.view.inputmethod.InputMethodManager
|
||
import android.widget.EditText
|
||
import android.widget.PopupWindow
|
||
import android.widget.SeekBar
|
||
import android.widget.TextView
|
||
import androidx.annotation.ColorRes
|
||
import androidx.core.content.ContextCompat
|
||
import androidx.core.text.HtmlCompat
|
||
import androidx.fragment.app.Fragment
|
||
import androidx.fragment.app.FragmentActivity
|
||
import androidx.lifecycle.*
|
||
import androidx.lifecycle.Observer
|
||
import androidx.recyclerview.widget.RecyclerView
|
||
import androidx.viewpager.widget.ViewPager
|
||
import com.airbnb.lottie.LottieAnimationView
|
||
import com.facebook.drawee.view.SimpleDraweeView
|
||
import com.gh.common.DefaultUrlHandler
|
||
import com.gh.common.constant.Config
|
||
import com.gh.common.constant.Constants
|
||
import com.gh.common.view.CenterImageSpan
|
||
import com.gh.common.view.CustomLinkMovementMethod
|
||
import com.gh.common.view.ExpandTextView
|
||
import com.gh.gamecenter.BuildConfig
|
||
import com.gh.gamecenter.R
|
||
import com.gh.gamecenter.WebActivity
|
||
import com.gh.gamecenter.manager.UserManager
|
||
import com.google.gson.reflect.TypeToken
|
||
import com.halo.assistant.HaloApp
|
||
import com.lightgame.download.DownloadEntity
|
||
import com.lightgame.utils.Utils
|
||
import io.reactivex.Observable
|
||
import io.reactivex.ObservableTransformer
|
||
import io.reactivex.SingleTransformer
|
||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||
import io.reactivex.disposables.Disposable
|
||
import io.reactivex.schedulers.Schedulers
|
||
import okhttp3.MediaType
|
||
import okhttp3.RequestBody
|
||
import java.net.URI
|
||
import java.util.*
|
||
import java.util.concurrent.TimeUnit
|
||
import java.util.regex.Pattern
|
||
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)
|
||
|
||
/**
|
||
* 创建以 activity 为观察者上下文的 viewModel
|
||
*/
|
||
inline fun <reified VM : ViewModel> FragmentActivity.viewModelProviderFromParent(
|
||
provider: ViewModelProvider.Factory? = null
|
||
) =
|
||
ViewModelProviders.of(this, 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)
|
||
}
|
||
|
||
|
||
/**
|
||
* Fragment related
|
||
*/
|
||
inline fun <reified T : Fragment> Fragment.fragmentFromActivity() =
|
||
parentFragmentManager.findFragmentByTag(T::class.java.simpleName) as? T
|
||
?: parentFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, T::class.java.canonicalName) as T
|
||
|
||
|
||
inline fun <reified T : Fragment> Fragment.fragmentFromParentFragment() =
|
||
childFragmentManager.findFragmentByTag(T::class.java.simpleName) as? T
|
||
?: childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, T::class.java.canonicalName) as T
|
||
|
||
|
||
/**
|
||
* 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
|
||
}
|
||
|
||
fun View.setRoundedColorBackground(@ColorRes color: Int, radius: Float) {
|
||
val shape = GradientDrawable()
|
||
shape.cornerRadius = radius.dip2px().toFloat()
|
||
shape.setColor(ContextCompat.getColor(context, color))
|
||
background = shape
|
||
}
|
||
|
||
fun View.setDebouncedClickListener(action: () -> Unit) {
|
||
setOnClickListener { debounceActionWithInterval(interval = 300L) { action.invoke() } }
|
||
}
|
||
|
||
fun isPublishEnv(): Boolean {
|
||
return BuildConfig.FLAVOR != "internal"
|
||
}
|
||
|
||
/**
|
||
* 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)
|
||
}
|
||
|
||
fun String.insert(index: Int, string: String): String {
|
||
return this.substring(0, index) + string + this.substring(index, this.length)
|
||
}
|
||
|
||
/**
|
||
* TextView 内部处理 ul li ol 得跟 Android 版本走,这里换成专属的标签手动处理
|
||
*/
|
||
fun String.replaceUnsupportedHtmlTag(): String {
|
||
return this.replace("<ul", "<hul")
|
||
.replace("</ul>", "</hul>")
|
||
.replace("<li", "<hli")
|
||
.replace("</li>", "</hli>")
|
||
.replace("<ol", "<hol")
|
||
.replace("</ol>", "</hol>")
|
||
}
|
||
|
||
fun String.containHtmlTag(): Boolean {
|
||
val pattern = Pattern.compile("<(\"[^\"]*\"|'[^']*'|[^'\">])*>")
|
||
val matcher = pattern.matcher(this)
|
||
return matcher.find()
|
||
}
|
||
|
||
/**
|
||
* 用户行为相关
|
||
*/
|
||
fun Fragment.showRegulationTestDialogIfNeeded(action: (() -> Unit)) {
|
||
if (UserManager.getInstance().userShouldTakeRegulationBaseOnLastRemind()) {
|
||
DialogUtils.showRegulationTestDialog(requireContext(),
|
||
{ DirectUtils.directToRegulationTestPage(requireContext()) },
|
||
{ action.invoke() })
|
||
} else {
|
||
action()
|
||
}
|
||
}
|
||
|
||
fun Context.showRegulationTestDialogIfNeeded(action: (() -> Unit)) {
|
||
if (UserManager.getInstance().userShouldTakeRegulationBaseOnLastRemind()) {
|
||
DialogUtils.showRegulationTestDialog(this,
|
||
{ DirectUtils.directToRegulationTestPage(this) },
|
||
{ action.invoke() })
|
||
} else {
|
||
action()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在限定 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()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 只在正式版本进行 try catch 操作
|
||
*
|
||
* 对于个别偶发的异常尽量不要做暴力的 try catch 处理
|
||
*/
|
||
inline fun tryCatchInRelease(action: (() -> Unit)) {
|
||
try {
|
||
action.invoke()
|
||
} catch (e: Throwable) {
|
||
if (BuildConfig.DEBUG) throw e
|
||
else e.printStackTrace()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在 debug 状态下抛出异常
|
||
*/
|
||
fun throwExceptionInDebug(message: String = "", predicate: Boolean = true) {
|
||
if (predicate && BuildConfig.DEBUG) {
|
||
throw RuntimeException(message)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在自动打包的包里弹 toast
|
||
*/
|
||
fun toastInInternalRelease(content: String) {
|
||
if (BuildConfig.BUILD_TIME != 0L) {
|
||
Utils.toast(HaloApp.getInstance(), content)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 主动抛出异常
|
||
*/
|
||
fun throwException(message: String = "", predicate: Boolean = true) {
|
||
if (predicate) {
|
||
throw RuntimeException(message)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* String related
|
||
*/
|
||
fun String.fromHtml(): Spanned {
|
||
return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||
}
|
||
|
||
// 将双引号换成单引号避免 JSON 解析异常 ( escape 后会有其它异常,暂时先替换成单引号了)
|
||
fun String.eliminateDoubleQuote(): String {
|
||
return this.replace("\"", "'")
|
||
}
|
||
|
||
// 去掉文章/答案的插入内容
|
||
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.countOccurrences(char: String): Int {
|
||
return StringTokenizer(" $this ", char).countTokens() - 1
|
||
}
|
||
|
||
fun String.getFirstElementDividedByDivider(divider: String): String {
|
||
if (this.contains(divider)) {
|
||
return this.split(divider.toRegex()).toTypedArray()[0]
|
||
}
|
||
return this
|
||
}
|
||
|
||
fun String.copyText() {
|
||
this.copyTextAndToast("")
|
||
}
|
||
|
||
fun String.copyTextAndToast(toastText: String = "复制成功") {
|
||
try {
|
||
val application = HaloApp.getInstance().application
|
||
val cmb = application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||
val clip = ClipData.newPlainText(null, this)
|
||
cmb.setPrimaryClip(clip)
|
||
|
||
if (!TextUtils.isEmpty(toastText)) {
|
||
ToastUtils.showToast(toastText)
|
||
}
|
||
} catch (e: SecurityException) {
|
||
// 在一些情况下会报以下这样的错误,可能是以浮动窗口显示然后没有焦点?(https://developer.android.com/about/versions/10/privacy/changes#clipboard-data)
|
||
// java.lang.Throwable: java.lang.SecurityException: com.xunmeng.pinduoduo from uid 10317 not allowed to perform READ_CLIPBOARD
|
||
ToastUtils.showToast("复制失败,请重试")
|
||
}
|
||
}
|
||
|
||
fun Map<String, String>.createRequestBody(): RequestBody {
|
||
val json = GsonUtils.toJson(this)
|
||
return RequestBody.create(MediaType.parse("application/json"), json)
|
||
}
|
||
|
||
fun Map<String, Any>.createRequestBodyAny(): RequestBody {
|
||
val json = GsonUtils.toJson(this)
|
||
return RequestBody.create(MediaType.parse("application/json"), json)
|
||
}
|
||
|
||
fun Any.toRequestBody(): 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()
|
||
}
|
||
|
||
fun Float.sp2px(): Int {
|
||
val scale: Float = HaloApp.getInstance().application.resources.displayMetrics.scaledDensity
|
||
return (this * scale + 0.5f).toInt()
|
||
}
|
||
|
||
|
||
/**
|
||
* PopupWindow 自动适配方向
|
||
* 弹出与锚点右对齐
|
||
*/
|
||
fun PopupWindow.showAutoOrientation(anchorView: View, distanceY: Int = 0, distanceX: 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
|
||
// 测量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
|
||
windowPos[1] = if (isNeedShowUp) {
|
||
anchorLoc[1] - windowHeight
|
||
} else anchorLoc[1] + anchorHeight
|
||
windowPos[0] = anchorLoc[0] - windowWidth + anchorView.width - distanceX
|
||
|
||
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 Fragment.checkCalendarPermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkCalendarPermissionBeforeAction(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()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun FragmentActivity.checkCalendarPermissionBeforeAction(action: (() -> Unit)) {
|
||
PermissionHelper.checkCalendarPermissionBeforeAction(this, object : EmptyCallback {
|
||
override fun onCallback() {
|
||
action.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* List related.
|
||
*/
|
||
|
||
// Returns the second element, or `null` if the list is empty.
|
||
fun <T> List<T>.secondOrNull(): T? {
|
||
return if (isEmpty() || size == 1) null else this[1]
|
||
}
|
||
|
||
/**
|
||
* TextView related.
|
||
*/
|
||
fun TextView.setTextWithHighlightedTextWrappedInsideWrapper(text: CharSequence,
|
||
wrapper: String = Constants.DEFAULT_TEXT_WRAPPER,
|
||
@ColorRes
|
||
highlightColorId: Int = R.color.theme_font,
|
||
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)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* List related
|
||
*/
|
||
fun <T> List<T>.safelyGetInRelease(index: Int): T? {
|
||
return if (index >= size) {
|
||
throwExceptionInDebug("这里触发了数组越界,请检查 (index $index >= size $size)")
|
||
toastInInternalRelease("这个操作可能触发闪退,请确定复现方式并联系开发处理")
|
||
null
|
||
} else {
|
||
try {
|
||
this[index]
|
||
} catch (e : IndexOutOfBoundsException){
|
||
e.printStackTrace()
|
||
null
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 拦截 TextView 中的 Url Span,用应用内页面的形式打开链接
|
||
* @param shrankText 未展开时的文字
|
||
* @param expandedText 展开后的文字
|
||
*/
|
||
fun ExpandTextView.setTextWithInterceptingInternalUrl(shrankText: CharSequence, expandedText: CharSequence) {
|
||
var shrankSsb = shrankText.interceptUrlSpanAndRoundImageSpan()
|
||
var expandedSsb = expandedText.interceptUrlSpanAndRoundImageSpan()
|
||
|
||
// 去掉旧版本 Android 系统 [Html.FROM_HTML_MODE_LEGACY] 产生的两个换行符 (丑陋的代码)
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||
while (shrankSsb.contains("\n\n")) {
|
||
val index = shrankSsb.indexOf("\n\n", 0, true)
|
||
shrankSsb = SpannableStringBuilder(shrankSsb.subSequence(0, index)).append(shrankSsb.subSequence(index + "\n".length, shrankSsb.length))
|
||
}
|
||
while (expandedSsb.contains("\n\n")) {
|
||
val index = expandedSsb.indexOf("\n\n", 0, true)
|
||
expandedSsb = SpannableStringBuilder(expandedSsb.subSequence(0, index)).append(expandedSsb.subSequence(index + "\n".length, expandedSsb.length))
|
||
}
|
||
}
|
||
|
||
// 去掉多余的 P 标签换行
|
||
if (expandedSsb.endsWith("\n", true)) {
|
||
expandedSsb = SpannableStringBuilder((expandedSsb.subSequence(0, expandedSsb.length - "\n".length)))
|
||
}
|
||
|
||
movementMethod = CustomLinkMovementMethod.getInstance()
|
||
|
||
shrankSsb = TextHelper.updateSpannableStringWithHighlightedSpan(context, shrankSsb, highlightedTextClickListener = null)
|
||
expandedSsb = TextHelper.updateSpannableStringWithHighlightedSpan(context, expandedSsb, highlightedTextClickListener = null)
|
||
setShrankTextAndExpandedText(shrankSsb, expandedSsb)
|
||
}
|
||
|
||
fun CharSequence.interceptUrlSpanAndRoundImageSpan(): SpannableStringBuilder {
|
||
return SpannableStringBuilder.valueOf(this).apply {
|
||
getSpans(0, length, URLSpan::class.java).forEach {
|
||
setSpan(
|
||
object : ClickableSpan() {
|
||
override fun updateDrawState(ds: TextPaint) {
|
||
super.updateDrawState(ds)
|
||
ds.color = ContextCompat.getColor(HaloApp.getInstance().application, R.color.theme_font)
|
||
ds.isUnderlineText = false
|
||
}
|
||
|
||
override fun onClick(widget: View) {
|
||
if (!DefaultUrlHandler.interceptUrl(widget.context, it.url, "")) {
|
||
widget.context.startActivity(WebActivity.getIntent(widget.context, it.url, true))
|
||
}
|
||
}
|
||
},
|
||
getSpanStart(it),
|
||
getSpanEnd(it),
|
||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||
)
|
||
removeSpan(it)
|
||
}
|
||
|
||
getSpans(0, length, ImageSpan::class.java).forEach {
|
||
setSpan(
|
||
CenterImageSpan(it.drawable),
|
||
getSpanStart(it),
|
||
getSpanEnd(it),
|
||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||
)
|
||
removeSpan(it)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun Int.toColor(): Int {
|
||
return ContextCompat.getColor(HaloApp.getInstance().application, 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)
|
||
}
|
||
|
||
/**
|
||
* DownloadEntity extension
|
||
*/
|
||
fun DownloadEntity.addMetaExtra(key: String, value: String?) {
|
||
value?.let {
|
||
if (meta == null) {
|
||
meta = hashMapOf()
|
||
}
|
||
meta[key] = value
|
||
}
|
||
}
|
||
|
||
fun DownloadEntity.getMetaExtra(key: String): String {
|
||
return meta?.get(key) ?: ""
|
||
}
|
||
|
||
fun DownloadEntity.isSilentUpdate(): Boolean {
|
||
return Constants.SILENT_UPDATE == getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE)
|
||
}
|
||
|
||
fun DownloadEntity.isSimulatorDownload(): Boolean {
|
||
return Constants.SIMULATOR_DOWNLOAD == getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE)
|
||
}
|
||
|
||
fun DownloadEntity.isSimulatorGame(): Boolean {
|
||
return getMetaExtra(Constants.SIMULATOR_GAME).isNotEmpty()
|
||
}
|
||
|
||
/**
|
||
* Process related
|
||
*/
|
||
fun Context.doOnMainProcessOnly(callback: EmptyCallback) {
|
||
doOnMainProcessOnly { callback.onCallback() }
|
||
}
|
||
|
||
/**
|
||
* 虽然现在我们没有了友盟以后只是单进程APP,但在 debug 模式下还有 whatTheStack 这个进程如果不限定主进程会出现奇奇怪怪的问题 (BroadcastReceiver相关)
|
||
*/
|
||
inline fun Context.doOnMainProcessOnly(f: () -> Unit) {
|
||
val processName = PackageUtils.obtainProcessName(this)
|
||
if (processName == null || BuildConfig.APPLICATION_ID == processName) {
|
||
f.invoke()
|
||
} else {
|
||
tryWithDefaultCatch {
|
||
Utils.log("Block one useless sub process method call from ${Thread.currentThread().stackTrace[3].methodName} -> ${Thread.currentThread().stackTrace[2].methodName}.")
|
||
}
|
||
}
|
||
}
|
||
|
||
inline fun doOnMainProcessOnly(f: () -> Unit) {
|
||
val processName = PackageUtils.obtainProcessName(HaloApp.getInstance().application)
|
||
if (processName == null || BuildConfig.APPLICATION_ID == processName) {
|
||
f.invoke()
|
||
} else {
|
||
tryWithDefaultCatch {
|
||
Utils.log("Block one useless sub process method call from ${Thread.currentThread().stackTrace[3].methodName} -> ${Thread.currentThread().stackTrace[2].methodName}.")
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试用包裹
|
||
*/
|
||
inline fun debugOnly(f: () -> Unit) {
|
||
if (BuildConfig.DEBUG) {
|
||
f()
|
||
}
|
||
}
|
||
|
||
inline fun testChannelOnly(f: () -> Unit) {
|
||
if (HaloApp.getInstance().channel == Config.DEFAULT_CHANNEL) {
|
||
f()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 倒计时,单位s
|
||
*/
|
||
inline fun countDownTimer(
|
||
timeInSeconds: Long,
|
||
crossinline block: (finish: Boolean, remainingTime: Long) -> Unit
|
||
): Disposable {
|
||
var subscribe: Disposable? = null
|
||
subscribe = Observable.interval(0, 1000, TimeUnit.MILLISECONDS)
|
||
.observeOn(AndroidSchedulers.mainThread())
|
||
.subscribe {
|
||
if (it < timeInSeconds) {
|
||
block.invoke(false, timeInSeconds - it)
|
||
} else {
|
||
block.invoke(true, 0)
|
||
if (subscribe != null && !subscribe!!.isDisposed) {
|
||
subscribe?.dispose()
|
||
}
|
||
}
|
||
}
|
||
return subscribe
|
||
}
|
||
|
||
/**
|
||
* 正计时
|
||
* @start 起始时间
|
||
*/
|
||
inline fun countUpTimer(
|
||
start: Long,
|
||
period: Long = 1000,
|
||
crossinline block: (millisUntilFinished: Long) -> Unit
|
||
): Disposable {
|
||
var startTime = start
|
||
return Observable.interval(0, period, TimeUnit.MILLISECONDS)
|
||
.observeOn(AndroidSchedulers.mainThread())
|
||
.subscribe {
|
||
startTime += period
|
||
block.invoke(startTime)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计时器(注意不需要的时候要取消订阅)
|
||
*/
|
||
inline fun rxTimer(interval: Long, crossinline block: (times: Long) -> Unit): Disposable {
|
||
return Observable.interval(0, interval, TimeUnit.MILLISECONDS)
|
||
.observeOn(AndroidSchedulers.mainThread())
|
||
.subscribe {
|
||
block.invoke(it)
|
||
}
|
||
}
|
||
|
||
fun LottieAnimationView.doOnAnimationEnd(action: () -> Unit) {
|
||
this.addAnimatorListener(object : Animator.AnimatorListener {
|
||
override fun onAnimationRepeat(animation: Animator?) {
|
||
|
||
}
|
||
|
||
override fun onAnimationEnd(animation: Animator?) {
|
||
action.invoke()
|
||
}
|
||
|
||
override fun onAnimationCancel(animation: Animator?) {
|
||
|
||
}
|
||
|
||
override fun onAnimationStart(animation: Animator?) {
|
||
|
||
}
|
||
})
|
||
}
|
||
|
||
fun String?.getExtension(): String? {
|
||
this ?: return null
|
||
|
||
tryCatchInRelease {
|
||
val lastDotIndex = this.lastIndexOf('.')
|
||
return if (lastDotIndex == -1) null else this.substring(lastDotIndex + 1)
|
||
}
|
||
return null
|
||
}
|
||
|
||
|
||
/**
|
||
* 检查内容是否一致
|
||
* @return true:相同 false:不同
|
||
*/
|
||
fun List<String>?.checkSameFromStringArray(check2: List<String>?): Boolean {
|
||
if (this == check2) {
|
||
return true
|
||
}
|
||
if (this == null && check2 == null) {
|
||
return true
|
||
}
|
||
if (this == null || check2 == null) {
|
||
return false
|
||
}
|
||
if (this.size != check2.size) {
|
||
return false
|
||
}
|
||
for (tag in this) {
|
||
if (!check2.contains(tag)) return false
|
||
}
|
||
for (tag in check2) {
|
||
if (!this.contains(tag)) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* EditText弹出软键盘
|
||
*/
|
||
fun EditText.showKeyBoard() {
|
||
this.postDelayed({
|
||
this.requestFocus()
|
||
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||
inputMethodManager.showSoftInput(this, 0)
|
||
}, 300)
|
||
}
|
||
|
||
|
||
fun SeekBar.doOnSeekBarChangeListener(progressChange: ((progress: Int) -> Unit)? = null, onStopTrackingTouch: (() -> Unit)? = null) {
|
||
this.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||
progressChange?.invoke(progress)
|
||
}
|
||
|
||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||
|
||
}
|
||
|
||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||
onStopTrackingTouch?.invoke()
|
||
}
|
||
})
|
||
}
|
||
|
||
fun <T> observableToMain(): ObservableTransformer<T, T> {
|
||
return ObservableTransformer { upstream ->
|
||
upstream.subscribeOn(Schedulers.io())
|
||
.observeOn(AndroidSchedulers.mainThread())
|
||
}
|
||
}
|
||
|
||
fun <T> singleToMain(): SingleTransformer<T, T> {
|
||
return SingleTransformer { upstream ->
|
||
upstream.subscribeOn(Schedulers.io())
|
||
.observeOn(AndroidSchedulers.mainThread())
|
||
}
|
||
} |