画面表示中はToastのかわりにPopupWindowを表示する

This commit is contained in:
tateisu 2022-07-23 21:55:07 +09:00
parent 4cb780909d
commit b6f404145a
11 changed files with 194 additions and 11 deletions

View File

@ -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")
} }

View File

@ -452,14 +452,15 @@ 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)) {
tmpList.add(0, newTag) tmpList.add(0, newTag)
} }
else -> tmpList.indexOfFirst(tagFinder) else -> tmpList.indexOfFirst(tagFinder)
.takeIf { it>=0 }?.let{ tmpList.removeAt(it)} .takeIf { it >= 0 }?.let { tmpList.removeAt(it) }
} }
} }
listData.clear() listData.clear()

View File

@ -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()

View File

@ -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

View File

@ -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>

View File

@ -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>