mirror of
https://github.com/tateisu/SubwayTooter
synced 2024-12-22 23:28:15 +01:00
画面表示中はToastのかわりにPopupWindowを表示する
This commit is contained in:
parent
4cb780909d
commit
b6f404145a
@ -51,6 +51,7 @@ class App1 : Application() {
|
||||
override fun onCreate() {
|
||||
log.d("onCreate")
|
||||
super.onCreate()
|
||||
initializeToastUtils(this)
|
||||
prepare(applicationContext, "App1.onCreate")
|
||||
}
|
||||
|
||||
|
@ -452,14 +452,15 @@ fun Column.onTagFollowChanged(account: SavedAccount, newTag: TootTag) {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
true ->
|
||||
if (tmpList.none(tagFinder)) {
|
||||
tmpList.add(0, newTag)
|
||||
}
|
||||
else -> tmpList.indexOfFirst(tagFinder)
|
||||
.takeIf { it>=0 }?.let{ tmpList.removeAt(it)}
|
||||
.takeIf { it >= 0 }?.let { tmpList.removeAt(it) }
|
||||
}
|
||||
}
|
||||
listData.clear()
|
||||
|
@ -1,21 +1,177 @@
|
||||
package jp.juggler.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
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 androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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 java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private val log = LogCategory("ToastUtils")
|
||||
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 {
|
||||
runOnMainLooper {
|
||||
|
||||
// Android 12以降はトーストを全文表示しないので、何か画面が表示中ならポップアップウィンドウを使う
|
||||
lastActivity?.get()?.let {
|
||||
try {
|
||||
showPopup(it, bLong, message)
|
||||
return@runOnMainLooper
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex, "showPopup failed.")
|
||||
}
|
||||
}
|
||||
// 画面がない、または失敗したら普通のトーストにフォールバック
|
||||
|
||||
// 前回のトーストの表示を終了する
|
||||
try {
|
||||
refToast?.get()?.cancel()
|
||||
|
@ -22,6 +22,7 @@ import android.view.WindowManager
|
||||
import android.view.WindowManager.BadTokenException
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import jp.juggler.util.LogCategory
|
||||
|
||||
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 {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use Context.getDisplay() instead.")
|
||||
override fun getDefaultDisplay(): Display? =
|
||||
base.defaultDisplay
|
||||
|
||||
@ -87,10 +89,11 @@ internal class SafeToastContext(base: Context, private val toast: Toast) : Conte
|
||||
@Suppress("TooManyFunctions")
|
||||
class ToastCompat private constructor(
|
||||
context: Context,
|
||||
private val base: Toast
|
||||
private val base: Toast,
|
||||
) : Toast(context) {
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("ToastCompat")
|
||||
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
private fun setContextCompat(view: View?, contextCreator: () -> Context) {
|
||||
@ -99,9 +102,8 @@ class ToastCompat private constructor(
|
||||
val field = View::class.java.getDeclaredField("mContext")
|
||||
field.isAccessible = true
|
||||
field[view] = contextCreator()
|
||||
} catch (throwable: Throwable) {
|
||||
@Suppress("PrintStackTrace")
|
||||
throwable.printStackTrace()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,8 +131,7 @@ class ToastCompat private constructor(
|
||||
* @param duration How long to display the message. Either [.LENGTH_SHORT] or
|
||||
* [.LENGTH_LONG]
|
||||
*/
|
||||
@SuppressLint("ShowToast")
|
||||
@Suppress("DEPRECATION")
|
||||
@Suppress("ShowToast", "DEPRECATION")
|
||||
fun makeText(context: Context, text: CharSequence?, duration: Int): ToastCompat {
|
||||
// We cannot pass the SafeToastContext to Toast.makeText() because
|
||||
// the View will unwrap the base context and we are in vain.
|
||||
@ -148,12 +149,14 @@ class ToastCompat private constructor(
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated(message = "Custom toast views are deprecated in API level 30.")
|
||||
override fun setView(view: View) {
|
||||
base.view = view
|
||||
setContextCompat(base.view) { SafeToastContext(view.context, base) }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated(message = "Custom toast views are deprecated in API level 30.")
|
||||
override fun getView(): View? = base.view
|
||||
|
||||
override fun show() = base.show()
|
||||
|
BIN
app/src/main/res/drawable-hdpi/ic_app_popup.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_app_popup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_app_popup.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_app_popup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_app_popup.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_app_popup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_app_popup.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_app_popup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_app_popup.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_app_popup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
22
app/src/main/res/layout/popup_toast.xml
Normal file
22
app/src/main/res/layout/popup_toast.xml
Normal 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>
|
@ -34,7 +34,7 @@
|
||||
|
||||
<color name="Light_colorReplyBackground">#cccccc</color>
|
||||
|
||||
<color name="Light_colorRefreshErrorBg">#8333</color>
|
||||
<color name="Light_colorRefreshErrorBg">#D222</color>
|
||||
|
||||
|
||||
<color name="Light_colorPostFormBackground">#eee</color>
|
||||
@ -62,7 +62,7 @@
|
||||
<color name="Dark_colorColumnListItemText">#66FFFFFF</color>
|
||||
<color name="Dark_colorTimeSmall">#BBBBBB</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_colorColumnHeaderPageNumber">#e4e4e4</color>
|
||||
|
Loading…
Reference in New Issue
Block a user