|
|
@@ -1,367 +1,152 @@
|
|
|
package com.adealink.weparty.commonui.toast
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
-import android.annotation.TargetApi
|
|
|
-import android.app.Activity
|
|
|
-import android.app.AppOpsManager
|
|
|
-import android.app.NotificationManager
|
|
|
import android.content.Context
|
|
|
-import android.content.res.Configuration
|
|
|
-import android.graphics.PixelFormat
|
|
|
-import android.graphics.Rect
|
|
|
-import android.os.Build
|
|
|
import android.os.Handler
|
|
|
-import android.os.Message
|
|
|
-import android.util.Log
|
|
|
+import android.os.Looper
|
|
|
import android.view.Gravity
|
|
|
-import android.view.WindowManager
|
|
|
-import android.widget.FrameLayout
|
|
|
+import android.view.LayoutInflater
|
|
|
+import android.view.View
|
|
|
import android.widget.Toast
|
|
|
-import com.adealink.frame.util.ActivityLifecycleCallbacksExt
|
|
|
-import com.adealink.frame.util.AppUtil
|
|
|
-import com.adealink.frame.util.DisplayUtil
|
|
|
-import com.adealink.frame.util.isMIUI
|
|
|
-import com.adealink.frame.util.runOnUiThread
|
|
|
-import java.lang.reflect.Field
|
|
|
-import java.lang.reflect.InvocationTargetException
|
|
|
-import java.lang.reflect.Modifier
|
|
|
+import androidx.annotation.DrawableRes
|
|
|
+import androidx.appcompat.widget.AppCompatImageView
|
|
|
+import androidx.appcompat.widget.AppCompatTextView
|
|
|
+import com.adealink.weparty.R
|
|
|
+import com.adealink.weparty.commonui.ext.dp
|
|
|
+import java.lang.ref.WeakReference
|
|
|
|
|
|
-object ToastCompat {
|
|
|
-
|
|
|
- private const val TAG = "ToastCompat"
|
|
|
-
|
|
|
- private const val LONG_DELAY = 3500
|
|
|
- private const val SHORT_DELAY = 2000 // 2 seconds
|
|
|
-
|
|
|
- private const val UNKNOWN = -1
|
|
|
- private const val DISABLE = 0
|
|
|
- private const val ENABLE = 1
|
|
|
-
|
|
|
- private const val CHECK_OP_NO_THROW = "checkOpNoThrow"
|
|
|
- private const val OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION"
|
|
|
-
|
|
|
- private var sToastEnable = UNKNOWN
|
|
|
- private var sUseView = false
|
|
|
-
|
|
|
- init {
|
|
|
- AppUtil.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksExt {
|
|
|
+private const val ATOMIC_EVENT_ID = 100011
|
|
|
+private const val FRAMEWORK_NAME = "AtomicXCore"
|
|
|
|
|
|
- override fun onEnterBackGround() {
|
|
|
- sToastEnable = UNKNOWN
|
|
|
- }
|
|
|
+object ToastCompat {
|
|
|
|
|
|
- })
|
|
|
+ enum class Style {
|
|
|
+ TEXT,
|
|
|
+ INFO,
|
|
|
+ HELP,
|
|
|
+ LOADING,
|
|
|
+ SUCCESS,
|
|
|
+ WARNING,
|
|
|
+ ERROR
|
|
|
}
|
|
|
|
|
|
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
|
|
- fun show(toast: Toast) {
|
|
|
- // API 30 之后纯 Text 的 Toast 中的 View 为空, 直接调用系统的方法(虽然应该是没有用的)
|
|
|
- if (toast.view == null) {
|
|
|
- toast.show()
|
|
|
- return
|
|
|
- }
|
|
|
- val context: Context = AppUtil.appContext
|
|
|
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 && !sUseView && !isMIUI()) {
|
|
|
- val windowManager: WindowManager? =
|
|
|
- AppUtil.getSystemService<WindowManager>(Context.WINDOW_SERVICE)
|
|
|
- // defined that sets up the layout params appropriately.
|
|
|
- val params = WindowManager.LayoutParams()
|
|
|
- params.height = WindowManager.LayoutParams.WRAP_CONTENT
|
|
|
- params.width = WindowManager.LayoutParams.WRAP_CONTENT
|
|
|
- params.format = PixelFormat.TRANSLUCENT
|
|
|
- val animation = context.resources.getIdentifier("Animation_Toast", "style", "android")
|
|
|
- if (animation > 0) {
|
|
|
- params.windowAnimations = animation
|
|
|
- }
|
|
|
- params.type = WindowManager.LayoutParams.TYPE_TOAST
|
|
|
- params.title = "Toast"
|
|
|
- params.packageName = context.packageName
|
|
|
- params.flags =
|
|
|
- WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
|
|
- params.horizontalMargin = toast.horizontalMargin
|
|
|
- params.verticalMargin = toast.verticalMargin
|
|
|
- val config = context.resources.configuration
|
|
|
- val gravity = Gravity.getAbsoluteGravity(toast.gravity, config.layoutDirection)
|
|
|
- params.gravity = gravity
|
|
|
- if (gravity and Gravity.HORIZONTAL_GRAVITY_MASK == Gravity.FILL_HORIZONTAL) {
|
|
|
- params.horizontalWeight = 1.0f
|
|
|
- }
|
|
|
- if (gravity and Gravity.VERTICAL_GRAVITY_MASK == Gravity.FILL_VERTICAL) {
|
|
|
- params.verticalWeight = 1.0f
|
|
|
- }
|
|
|
- params.x = toast.xOffset
|
|
|
- params.y = toast.yOffset
|
|
|
- try {
|
|
|
- windowManager?.addView(toast.view, params)
|
|
|
- } catch (e: WindowManager.BadTokenException) {
|
|
|
- e.printStackTrace()
|
|
|
- sUseView = true
|
|
|
- }
|
|
|
- if (!sUseView) {
|
|
|
- runOnUiThread({ windowManager?.removeView(toast.view) },
|
|
|
- (if (toast.duration == Toast.LENGTH_LONG) LONG_DELAY else SHORT_DELAY).toLong())
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- val activity: Activity? = AppUtil.currentActivity
|
|
|
- if (activity == null) {
|
|
|
- Log.e(TAG, "activity is null")
|
|
|
- } else {
|
|
|
- val root = activity.window.decorView
|
|
|
- val rect = Rect()
|
|
|
- root.getWindowVisibleDisplayFrame(rect)
|
|
|
- val statusBarHeight = rect.top
|
|
|
- if (root is FrameLayout) {
|
|
|
- val layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
|
- FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
|
- Gravity.CENTER_HORIZONTAL or toast.gravity)
|
|
|
- val orientation: Int = DisplayUtil.getOrientation()
|
|
|
- //虚拟栏存在的情况下居中调整
|
|
|
- if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
|
- val navHeight: Int = DisplayUtil.getScreenRealHeight(activity) - rect.bottom
|
|
|
- when {
|
|
|
- toast.gravity and Gravity.BOTTOM == Gravity.BOTTOM -> {
|
|
|
- layoutParams.bottomMargin = toast.yOffset + navHeight
|
|
|
- }
|
|
|
- toast.gravity and Gravity.TOP == Gravity.TOP -> {
|
|
|
- layoutParams.topMargin = toast.yOffset + statusBarHeight
|
|
|
- }
|
|
|
- toast.gravity and Gravity.CENTER == Gravity.CENTER -> {
|
|
|
- layoutParams.topMargin = toast.yOffset
|
|
|
- }
|
|
|
- else -> {
|
|
|
- layoutParams.bottomMargin = toast.yOffset + navHeight
|
|
|
- }
|
|
|
- }
|
|
|
- when {
|
|
|
- toast.gravity and Gravity.START == Gravity.START -> {
|
|
|
- layoutParams.marginStart = toast.xOffset
|
|
|
- }
|
|
|
- toast.gravity and Gravity.END == Gravity.END -> {
|
|
|
- layoutParams.marginEnd = toast.xOffset
|
|
|
- }
|
|
|
- else -> {
|
|
|
- layoutParams.leftMargin = toast.xOffset
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- val config = context.resources.configuration
|
|
|
- val gravity = Gravity.getAbsoluteGravity(toast.gravity, config.layoutDirection)
|
|
|
- val navPositionRight = rect.left == 0
|
|
|
- val navHeight =
|
|
|
- if (navPositionRight) DisplayUtil.getScreenRealWidth(activity) - rect.right else rect.left
|
|
|
- when {
|
|
|
- gravity and Gravity.BOTTOM == Gravity.BOTTOM -> {
|
|
|
- layoutParams.bottomMargin = toast.yOffset
|
|
|
- }
|
|
|
- gravity and Gravity.TOP == Gravity.TOP -> {
|
|
|
- layoutParams.topMargin = toast.yOffset + statusBarHeight
|
|
|
- }
|
|
|
- else -> {
|
|
|
- layoutParams.bottomMargin = toast.yOffset
|
|
|
- }
|
|
|
- }
|
|
|
- when {
|
|
|
- gravity and Gravity.LEFT == Gravity.LEFT -> {
|
|
|
- layoutParams.leftMargin =
|
|
|
- toast.xOffset + if (navPositionRight) 0 else navHeight
|
|
|
- }
|
|
|
- gravity and Gravity.RIGHT == Gravity.RIGHT -> {
|
|
|
- layoutParams.rightMargin =
|
|
|
- toast.xOffset + if (navPositionRight) navHeight else 0
|
|
|
- }
|
|
|
- else -> {
|
|
|
- layoutParams.leftMargin =
|
|
|
- toast.xOffset + if (navPositionRight) -navHeight / 2 else navHeight / 2
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- root.addView(toast.view, layoutParams)
|
|
|
- runOnUiThread({ root.removeView(toast.view) },
|
|
|
- (if (toast.duration == Toast.LENGTH_LONG) LONG_DELAY else SHORT_DELAY).toLong())
|
|
|
- }
|
|
|
- }
|
|
|
+ enum class Position {
|
|
|
+ TOP,
|
|
|
+ CENTER,
|
|
|
+ BOTTOM
|
|
|
}
|
|
|
|
|
|
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
|
|
- fun show(content: CharSequence?, duration: Int) {
|
|
|
- show(Toast.makeText(AppUtil.appContext, content, duration))
|
|
|
- }
|
|
|
+ enum class Duration(val value: Int) {
|
|
|
+ SHORT(Toast.LENGTH_SHORT),
|
|
|
+ LONG(Toast.LENGTH_LONG);
|
|
|
|
|
|
- fun isToastEnabled(): Boolean {
|
|
|
- if (sToastEnable == DISABLE) {
|
|
|
- return false
|
|
|
- }
|
|
|
- if (sToastEnable == ENABLE) {
|
|
|
- return true
|
|
|
- }
|
|
|
- val context: Context = AppUtil.appContext
|
|
|
- return when {
|
|
|
- Build.VERSION.SDK_INT >= 24 -> {
|
|
|
- val notificationManager: NotificationManager =
|
|
|
- AppUtil.getSystemService<NotificationManager>(Context.NOTIFICATION_SERVICE)
|
|
|
- ?: return false
|
|
|
- recordToastEnable(notificationManager.areNotificationsEnabled())
|
|
|
- }
|
|
|
- Build.VERSION.SDK_INT >= 19 -> {
|
|
|
- val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
|
|
- val appInfo = context.applicationInfo
|
|
|
- val pkg = context.applicationContext.packageName
|
|
|
- val uid = appInfo.uid
|
|
|
- try {
|
|
|
- val appOpsClass = Class.forName(AppOpsManager::class.java.name)
|
|
|
- val checkOpNoThrowMethod =
|
|
|
- appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE,
|
|
|
- String::class.java)
|
|
|
- val opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION)
|
|
|
- val value = opPostNotificationValue[Int::class.java] as Int
|
|
|
- recordToastEnable(checkOpNoThrowMethod.invoke(appOps,
|
|
|
- value,
|
|
|
- uid,
|
|
|
- pkg) as Int == AppOpsManager.MODE_ALLOWED)
|
|
|
- } catch (e: ClassNotFoundException) {
|
|
|
- recordToastEnable(true)
|
|
|
- } catch (e: NoSuchMethodException) {
|
|
|
- recordToastEnable(true)
|
|
|
- } catch (e: NoSuchFieldException) {
|
|
|
- recordToastEnable(true)
|
|
|
- } catch (e: InvocationTargetException) {
|
|
|
- recordToastEnable(true)
|
|
|
- } catch (e: IllegalAccessException) {
|
|
|
- recordToastEnable(true)
|
|
|
- } catch (e: RuntimeException) {
|
|
|
- recordToastEnable(true)
|
|
|
- }
|
|
|
- }
|
|
|
- else -> {
|
|
|
- recordToastEnable(true)
|
|
|
+ companion object {
|
|
|
+ @JvmStatic
|
|
|
+ fun map(duration: Int): Duration {
|
|
|
+ return entries.find { it.value == duration } ?: SHORT
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private fun recordToastEnable(enable: Boolean): Boolean {
|
|
|
- sToastEnable = if (enable) ENABLE else DISABLE
|
|
|
- return enable
|
|
|
- }
|
|
|
-
|
|
|
- @SuppressLint("ShowToast")
|
|
|
- fun makeText(context: Context?, text: CharSequence?, duration: Int): Toast {
|
|
|
- val toast = Toast.makeText(context, text, duration)
|
|
|
- if (Build.VERSION.SDK_INT == 25 || Build.VERSION.SDK_INT == 19) {
|
|
|
- tryToHack(toast)
|
|
|
+ private var toastRef: WeakReference<Toast>? = null
|
|
|
+ private var layoutRef: WeakReference<View>? = null
|
|
|
+
|
|
|
+ private val mainHandler = Handler(Looper.getMainLooper())
|
|
|
+ private var pendingTask: Runnable? = null
|
|
|
+
|
|
|
+ fun show(
|
|
|
+ context: Context,
|
|
|
+ text: String,
|
|
|
+ style: Style = Style.TEXT,
|
|
|
+ position: Position = Position.CENTER,
|
|
|
+ @DrawableRes customIcon: Int? = null,
|
|
|
+ duration: Duration = Duration.SHORT,
|
|
|
+ code: Int? = null,
|
|
|
+ extensionInfo: Map<String, Any>? = null
|
|
|
+ ) {
|
|
|
+ if (text.isBlank()) return
|
|
|
+
|
|
|
+ pendingTask?.let {
|
|
|
+ mainHandler.removeCallbacks(it)
|
|
|
+ pendingTask = null
|
|
|
}
|
|
|
- return toast
|
|
|
- }
|
|
|
|
|
|
- @SuppressLint("ShowToast")
|
|
|
- fun makeText(context: Context?, resId: Int, duration: Int): Toast {
|
|
|
- val toast = Toast.makeText(context, resId, duration)
|
|
|
- if (Build.VERSION.SDK_INT == 25 || Build.VERSION.SDK_INT == 19) {
|
|
|
- tryToHack(toast)
|
|
|
- }
|
|
|
- return toast
|
|
|
- }
|
|
|
+ val appContext = context.applicationContext
|
|
|
|
|
|
- fun tryToHack(toast: Toast) {
|
|
|
- try {
|
|
|
- val mTN = getFieldValue(toast, "mTN")
|
|
|
- if (mTN != null) {
|
|
|
- var isSuccess = false
|
|
|
- val rawShowRunnable = getFieldValue(mTN, "mShow")
|
|
|
- if (rawShowRunnable is Runnable) {
|
|
|
- isSuccess = setFieldValue(mTN, "mShow", InternalRunnable(rawShowRunnable))
|
|
|
- }
|
|
|
- if (!isSuccess) {
|
|
|
- val rawHandler = getFieldValue(mTN, "mHandler")
|
|
|
- if (rawHandler is Handler) {
|
|
|
- isSuccess = setFieldValue(rawHandler, "mCallback", InternalHandlerCallback(
|
|
|
- rawHandler))
|
|
|
- }
|
|
|
- }
|
|
|
- if (!isSuccess) {
|
|
|
- Log.e("ToastCompat", "tryToHack error.")
|
|
|
+ val task = object : Runnable {
|
|
|
+ override fun run() {
|
|
|
+ executeRealShow(appContext, text, style, position, customIcon, duration)
|
|
|
+ if (pendingTask === this) {
|
|
|
+ pendingTask = null
|
|
|
}
|
|
|
}
|
|
|
- } catch (var5: Throwable) {
|
|
|
- var5.printStackTrace()
|
|
|
}
|
|
|
+ pendingTask = task
|
|
|
+ mainHandler.postDelayed(task, 50)
|
|
|
}
|
|
|
|
|
|
- private fun setFieldValue(`object`: Any, fieldName: String, newFieldValue: Any): Boolean {
|
|
|
- val field = getDeclaredField(`object`, fieldName)
|
|
|
- if (field != null) {
|
|
|
- try {
|
|
|
- val accessFlags = field.modifiers
|
|
|
- if (Modifier.isFinal(accessFlags)) {
|
|
|
- val modifiersField = Field::class.java.getDeclaredField("accessFlags")
|
|
|
- modifiersField.isAccessible = true
|
|
|
- modifiersField.setInt(field, field.modifiers and -17)
|
|
|
- }
|
|
|
- if (!field.isAccessible) {
|
|
|
- field.isAccessible = true
|
|
|
- }
|
|
|
- field[`object`] = newFieldValue
|
|
|
- return true
|
|
|
- } catch (var6: Exception) {
|
|
|
- var6.printStackTrace()
|
|
|
- }
|
|
|
- }
|
|
|
- return false
|
|
|
- }
|
|
|
+ private fun executeRealShow(
|
|
|
+ appContext: Context,
|
|
|
+ text: String,
|
|
|
+ style: Style,
|
|
|
+ position: Position,
|
|
|
+ @DrawableRes customIcon: Int?,
|
|
|
+ duration: Duration,
|
|
|
+ ) {
|
|
|
|
|
|
- private fun getFieldValue(obj: Any, fieldName: String): Any? {
|
|
|
- val field = getDeclaredField(obj, fieldName)
|
|
|
- return getFieldValue(obj, field)
|
|
|
- }
|
|
|
+ var toast = toastRef?.get()
|
|
|
+ var layout = layoutRef?.get()
|
|
|
|
|
|
- private fun getFieldValue(obj: Any, field: Field?): Any? {
|
|
|
- if (field != null) {
|
|
|
- try {
|
|
|
- if (!field.isAccessible) {
|
|
|
- field.isAccessible = true
|
|
|
- }
|
|
|
- return field[obj]
|
|
|
- } catch (var3: Exception) {
|
|
|
- var3.printStackTrace()
|
|
|
- }
|
|
|
+ if (toast == null || layout == null) {
|
|
|
+ layout = LayoutInflater.from(appContext).inflate(R.layout.layout_compat_toast, null)
|
|
|
+ toast = Toast(appContext)
|
|
|
+
|
|
|
+ @Suppress("DEPRECATION")
|
|
|
+ toast.view = layout
|
|
|
+
|
|
|
+ layoutRef = WeakReference(layout)
|
|
|
+ toastRef = WeakReference(toast)
|
|
|
}
|
|
|
- return null
|
|
|
- }
|
|
|
|
|
|
- private fun getDeclaredField(obj: Any, fieldName: String): Field? {
|
|
|
- var superClass: Class<*> = obj.javaClass
|
|
|
- while (superClass != Any::class.java) {
|
|
|
- superClass = try {
|
|
|
- return superClass.getDeclaredField(fieldName)
|
|
|
- } catch (var4: NoSuchFieldException) {
|
|
|
- superClass.superclass
|
|
|
+ applyDesignTokens(layout, text, style, customIcon)
|
|
|
+
|
|
|
+ toast.apply {
|
|
|
+ this.duration = duration.value
|
|
|
+ val gravity = when (position) {
|
|
|
+ Position.TOP -> Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
|
|
+ Position.CENTER -> Gravity.CENTER
|
|
|
+ Position.BOTTOM -> Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
|
|
|
}
|
|
|
+ setGravity(gravity, 0, if (position == Position.CENTER) 0 else 24.dp())
|
|
|
+ show()
|
|
|
}
|
|
|
- return null
|
|
|
}
|
|
|
|
|
|
- private class InternalHandlerCallback(private val mHandler: Handler) :
|
|
|
- Handler.Callback {
|
|
|
- override fun handleMessage(msg: Message): Boolean {
|
|
|
- try {
|
|
|
- mHandler.handleMessage(msg)
|
|
|
- } catch (var3: Throwable) {
|
|
|
- Log.e("ToastCompat", "catch $var3")
|
|
|
- var3.printStackTrace()
|
|
|
- }
|
|
|
- return true
|
|
|
+ @SuppressLint("SetTextI18n")
|
|
|
+ private fun applyDesignTokens(
|
|
|
+ container: View,
|
|
|
+ text: String,
|
|
|
+ style: Style,
|
|
|
+ @DrawableRes customIcon: Int?,
|
|
|
+ ) {
|
|
|
+ val iconView = container.findViewById<AppCompatImageView>(R.id.iv_toast)
|
|
|
+ val textView = container.findViewById<AppCompatTextView>(R.id.tv_toast)
|
|
|
+
|
|
|
+ textView.text = text
|
|
|
+ val iconRes = customIcon ?: resolveIconRes(style)
|
|
|
+ if (iconRes != null) {
|
|
|
+ iconView.visibility = View.VISIBLE
|
|
|
+ iconView.setImageResource(iconRes)
|
|
|
+ } else {
|
|
|
+ iconView.visibility = View.GONE
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private class InternalRunnable(private val mRunnable: Runnable) : Runnable {
|
|
|
- override fun run() {
|
|
|
- try {
|
|
|
- mRunnable.run()
|
|
|
- } catch (var2: Throwable) {
|
|
|
- Log.e("ToastCompat", "catch $var2")
|
|
|
- var2.printStackTrace()
|
|
|
- }
|
|
|
+ @DrawableRes
|
|
|
+ private fun resolveIconRes(style: Style): Int? {
|
|
|
+ return when (style) {
|
|
|
+ Style.WARNING -> R.drawable.common_toast_warming_ic
|
|
|
+ else -> null
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
}
|