画面表示中はToastのかわりにPopupWindowを表示する
This commit is contained in:
parent
4cb780909d
commit
b6f404145a
|
@ -51,6 +51,7 @@ class App1 : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
log.d("onCreate")
|
log.d("onCreate")
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
initializeToastUtils(this)
|
||||||
prepare(applicationContext, "App1.onCreate")
|
prepare(applicationContext, "App1.onCreate")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -452,7 +452,8 @@ fun Column.onTagFollowChanged(account: SavedAccount, newTag: TootTag) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type == ColumnType.FOLLOWED_HASHTAGS) {
|
if (type == ColumnType.FOLLOWED_HASHTAGS) {
|
||||||
val tagFinder:(TimelineItem)->Boolean = {it is TootTag && it.name == newTag.name}
|
val tagFinder: (TimelineItem) -> Boolean =
|
||||||
|
{ it is TootTag && it.name == newTag.name }
|
||||||
when (newTag.following) {
|
when (newTag.following) {
|
||||||
true ->
|
true ->
|
||||||
if (tmpList.none(tagFinder)) {
|
if (tmpList.none(tagFinder)) {
|
||||||
|
|
|
@ -1,21 +1,177 @@
|
||||||
package jp.juggler.util
|
package jp.juggler.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.widget.PopupWindow
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import kotlinx.coroutines.CancellationException
|
import jp.juggler.subwaytooter.databinding.PopupToastBinding
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import me.drakeet.support.toast.ToastCompat
|
import me.drakeet.support.toast.ToastCompat
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
private val log = LogCategory("ToastUtils")
|
private val log = LogCategory("ToastUtils")
|
||||||
private var refToast: WeakReference<Toast>? = null
|
private var refToast: WeakReference<Toast>? = null
|
||||||
|
private var oldApplication: WeakReference<Application>? = null
|
||||||
|
private var lastActivity: WeakReference<Activity>? = null
|
||||||
|
private var lastPopup: WeakReference<PopupWindow>? = null
|
||||||
|
|
||||||
|
private val activityCallback = object : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
lastActivity = WeakReference(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
lastActivity = WeakReference(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityPaused(activity: Activity) {
|
||||||
|
if (lastActivity?.get() == activity) {
|
||||||
|
lastActivity = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) {
|
||||||
|
if (lastActivity?.get() == activity) {
|
||||||
|
lastActivity = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
|
if (lastActivity?.get() == activity) {
|
||||||
|
lastActivity = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App1.onCreateから呼ばれる
|
||||||
|
*/
|
||||||
|
fun initializeToastUtils(app: Application) {
|
||||||
|
try {
|
||||||
|
oldApplication?.get()?.unregisterActivityLifecycleCallbacks(activityCallback)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Log.e("SubwayTooter", "unregisterActivityLifecycleCallbacks failed.", ex)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
app.registerActivityLifecycleCallbacks(activityCallback)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Log.e("SubwayTooter", "registerActivityLifecycleCallbacks failed.", ex)
|
||||||
|
}
|
||||||
|
oldApplication = WeakReference(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animationを開始して終了を非同期待機する
|
||||||
|
*/
|
||||||
|
suspend fun Animation.startAndAwait(duration: Long, v: View) =
|
||||||
|
try {
|
||||||
|
withTimeout(duration + 333L) {
|
||||||
|
suspendCancellableCoroutine<Unit> { cont ->
|
||||||
|
v.clearAnimation()
|
||||||
|
this@startAndAwait.duration = duration
|
||||||
|
this@startAndAwait.fillAfter = true
|
||||||
|
setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationStart(animation: Animation?) {}
|
||||||
|
override fun onAnimationRepeat(animation: Animation?) {}
|
||||||
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
|
cont.resume(Unit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
v.startAnimation(this@startAndAwait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: TimeoutCancellationException) {
|
||||||
|
log.w(ex, "startAndAwait timeout.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPopup(activity: Activity, bLong: Boolean, message: String) {
|
||||||
|
val rootView = activity.findViewById<View?>(android.R.id.content)?.rootView
|
||||||
|
?: error("missing rootView")
|
||||||
|
|
||||||
|
val views = PopupToastBinding.inflate(activity.layoutInflater)
|
||||||
|
views.tvMessage.text = message
|
||||||
|
|
||||||
|
val popupWindow = PopupWindow(
|
||||||
|
views.root,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
// タップ時に他のViewでキャッチされないための設定
|
||||||
|
popupWindow.isFocusable = false
|
||||||
|
popupWindow.isTouchable = false
|
||||||
|
popupWindow.isOutsideTouchable = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastPopup?.get()?.dismiss()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.trace(ex, "dismiss failed.")
|
||||||
|
}
|
||||||
|
lastPopup = WeakReference(popupWindow)
|
||||||
|
|
||||||
|
popupWindow.showAtLocation(rootView, Gravity.CENTER, 0, 0)
|
||||||
|
|
||||||
|
launchMain {
|
||||||
|
|
||||||
|
// fade in
|
||||||
|
AlphaAnimation(0.1f, 1f)
|
||||||
|
.startAndAwait(333L, views.tvMessage)
|
||||||
|
|
||||||
|
// keep
|
||||||
|
val keepDuration = when {
|
||||||
|
bLong -> 4000L
|
||||||
|
else -> 2000L
|
||||||
|
}
|
||||||
|
delay(keepDuration)
|
||||||
|
|
||||||
|
// fade out
|
||||||
|
AlphaAnimation(1f, 0f)
|
||||||
|
.startAndAwait(333L, views.tvMessage)
|
||||||
|
|
||||||
|
// dismiss
|
||||||
|
try {
|
||||||
|
popupWindow.dismiss()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.e(ex, "dismiss failed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
|
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
|
||||||
runOnMainLooper {
|
runOnMainLooper {
|
||||||
|
|
||||||
|
// Android 12以降はトーストを全文表示しないので、何か画面が表示中ならポップアップウィンドウを使う
|
||||||
|
lastActivity?.get()?.let {
|
||||||
|
try {
|
||||||
|
showPopup(it, bLong, message)
|
||||||
|
return@runOnMainLooper
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.trace(ex, "showPopup failed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 画面がない、または失敗したら普通のトーストにフォールバック
|
||||||
|
|
||||||
// 前回のトーストの表示を終了する
|
// 前回のトーストの表示を終了する
|
||||||
try {
|
try {
|
||||||
refToast?.get()?.cancel()
|
refToast?.get()?.cancel()
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.WindowManager
|
||||||
import android.view.WindowManager.BadTokenException
|
import android.view.WindowManager.BadTokenException
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import jp.juggler.util.LogCategory
|
||||||
|
|
||||||
fun interface BadTokenListener {
|
fun interface BadTokenListener {
|
||||||
|
|
||||||
|
@ -58,6 +59,7 @@ internal class SafeToastContext(base: Context, private val toast: Toast) : Conte
|
||||||
private inner class WindowManagerWrapper(private val base: WindowManager) : WindowManager {
|
private inner class WindowManagerWrapper(private val base: WindowManager) : WindowManager {
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use Context.getDisplay() instead.")
|
||||||
override fun getDefaultDisplay(): Display? =
|
override fun getDefaultDisplay(): Display? =
|
||||||
base.defaultDisplay
|
base.defaultDisplay
|
||||||
|
|
||||||
|
@ -87,10 +89,11 @@ internal class SafeToastContext(base: Context, private val toast: Toast) : Conte
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class ToastCompat private constructor(
|
class ToastCompat private constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val base: Toast
|
private val base: Toast,
|
||||||
) : Toast(context) {
|
) : Toast(context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val log = LogCategory("ToastCompat")
|
||||||
|
|
||||||
@SuppressLint("DiscouragedPrivateApi")
|
@SuppressLint("DiscouragedPrivateApi")
|
||||||
private fun setContextCompat(view: View?, contextCreator: () -> Context) {
|
private fun setContextCompat(view: View?, contextCreator: () -> Context) {
|
||||||
|
@ -99,9 +102,8 @@ class ToastCompat private constructor(
|
||||||
val field = View::class.java.getDeclaredField("mContext")
|
val field = View::class.java.getDeclaredField("mContext")
|
||||||
field.isAccessible = true
|
field.isAccessible = true
|
||||||
field[view] = contextCreator()
|
field[view] = contextCreator()
|
||||||
} catch (throwable: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
@Suppress("PrintStackTrace")
|
log.trace(ex)
|
||||||
throwable.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,8 +131,7 @@ class ToastCompat private constructor(
|
||||||
* @param duration How long to display the message. Either [.LENGTH_SHORT] or
|
* @param duration How long to display the message. Either [.LENGTH_SHORT] or
|
||||||
* [.LENGTH_LONG]
|
* [.LENGTH_LONG]
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ShowToast")
|
@Suppress("ShowToast", "DEPRECATION")
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun makeText(context: Context, text: CharSequence?, duration: Int): ToastCompat {
|
fun makeText(context: Context, text: CharSequence?, duration: Int): ToastCompat {
|
||||||
// We cannot pass the SafeToastContext to Toast.makeText() because
|
// We cannot pass the SafeToastContext to Toast.makeText() because
|
||||||
// the View will unwrap the base context and we are in vain.
|
// the View will unwrap the base context and we are in vain.
|
||||||
|
@ -148,12 +149,14 @@ class ToastCompat private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated(message = "Custom toast views are deprecated in API level 30.")
|
||||||
override fun setView(view: View) {
|
override fun setView(view: View) {
|
||||||
base.view = view
|
base.view = view
|
||||||
setContextCompat(base.view) { SafeToastContext(view.context, base) }
|
setContextCompat(base.view) { SafeToastContext(view.context, base) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated(message = "Custom toast views are deprecated in API level 30.")
|
||||||
override fun getView(): View? = base.view
|
override fun getView(): View? = base.view
|
||||||
|
|
||||||
override fun show() = base.show()
|
override fun show() = base.show()
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal|bottom"
|
||||||
|
android:layout_marginBottom="80dp"
|
||||||
|
android:background="@drawable/bg_refresh_error"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textColor="#FFF"
|
||||||
|
app:drawableStartCompat="@drawable/ic_app_popup"
|
||||||
|
tools:text="テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例テキスト例" />
|
||||||
|
</FrameLayout>
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
<color name="Light_colorReplyBackground">#cccccc</color>
|
<color name="Light_colorReplyBackground">#cccccc</color>
|
||||||
|
|
||||||
<color name="Light_colorRefreshErrorBg">#8333</color>
|
<color name="Light_colorRefreshErrorBg">#D222</color>
|
||||||
|
|
||||||
|
|
||||||
<color name="Light_colorPostFormBackground">#eee</color>
|
<color name="Light_colorPostFormBackground">#eee</color>
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<color name="Dark_colorColumnListItemText">#66FFFFFF</color>
|
<color name="Dark_colorColumnListItemText">#66FFFFFF</color>
|
||||||
<color name="Dark_colorTimeSmall">#BBBBBB</color>
|
<color name="Dark_colorTimeSmall">#BBBBBB</color>
|
||||||
<color name="Dark_colorContentText">#dddddd</color>
|
<color name="Dark_colorContentText">#dddddd</color>
|
||||||
<color name="Dark_colorRefreshErrorBg">#8333</color>
|
<color name="Dark_colorRefreshErrorBg">#D222</color>
|
||||||
|
|
||||||
<color name="Dark_colorColumnHeaderAcct">#e4e4e4</color>
|
<color name="Dark_colorColumnHeaderAcct">#e4e4e4</color>
|
||||||
<color name="Dark_colorColumnHeaderPageNumber">#e4e4e4</color>
|
<color name="Dark_colorColumnHeaderPageNumber">#e4e4e4</color>
|
||||||
|
|
Loading…
Reference in New Issue