Toastの再利用

Androidで、サービス等のUIを持たないコンポーネントから画面にテキストを表示するToastというAPIがある。

割と昔からあるものなので挙動も同じだろうと思っていたら、某社の端末(未発売)でトラブルがあった。
その端末のバグっぽい印象だったので割と邪道な対応で済ませたが、それについてはあまり詳しく書けない。

ついでに手持ちの他の端末でも簡単なテストを行ってみた。

  • トースト を表示して4秒経過したらトーストをキャンセルして、トーストへの参照をnullで上書きする。
  • トースト を前回表示してから4秒未満で再度トーストを表示した時に、以下のいずれかの処理を行う。
    • キャンセルした後に再利用(setTextして再度show)
    • キャンセルせずに再利用(setTextして再度show)
    • キャンセルした後に新しくmakeTextしてshow
    • キャンセルせずに新しくmakeTextしてshow

f:id:tateisu:20181109234422j:plain

結果

キャンセルして再利用
    • 4.4.2~9.0 でうまくいかない。文章は更新されるが、最初のトーストが表示されてから4秒で消えてしまう。
キャンセルせずに再利用
    • 4.4.2の端末ではOK。最後に文章を更新してshowしてから4秒でトーストが消える。
    • 7.1.1の端末ではNG。文章は更新されるが、最初のトーストが表示されてから4秒で消えてしまう。
    • 8.0.0の端末ではNG。文章は更新される(+トーストの表示がちらつく)が、最初のトーストが表示されてから4秒で消えてしまう。
    • 9.0.0の端末ではNG。文章は更新される(+トーストの表示がちらつく)が、連打するとすぐにトーストが消えてしまう。
キャンセルした後に新しくmakeTextしてshow
    • 特に問題は見られなかった
キャンセルせずに新しくmakeTextしてshow
    • 端末によってはトーストがキューに溜まり遅延が発生する

コード例

package jp.juggler.toastsample

import android.os.Bundle
import android.os.Handler
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.Toast

class MainActivity : AppCompatActivity() {
	
	companion object {
		private const val LOGTAG = "ToastSample"
	}
	
	lateinit var handler : Handler
	
	lateinit var btnCancel : View
	
	private var nCount = 0
	
	private var lastToast : Toast? = null
	
	override fun onCreate(savedInstanceState : Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)
		handler = Handler()
		
		btnCancel = findViewById<View>(R.id.btnCancel)
		btnCancel.setOnClickListener { toastRemover.run() }
		btnCancel.isEnabled = false
		
		
		findViewById<View>(R.id.btnShow1).setOnClickListener { showToast(1) }
		findViewById<View>(R.id.btnShow2).setOnClickListener { showToast(2) }
		findViewById<View>(R.id.btnShow3).setOnClickListener { showToast(3) }
		findViewById<View>(R.id.btnShow4).setOnClickListener { showToast(4) }
	}
	
	private val toastRemover : Runnable = object : Runnable {
		override fun run() {
			handler.removeCallbacks(this)
			lastToast?.cancel()
			lastToast = null
			btnCancel.isEnabled = false
		}
	}
	
	private fun showToast(type : Int) {
		try {
			val gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
			val text = "Toast ${++ nCount}"
			val duration = Toast.LENGTH_LONG
			var toast = lastToast
			if(toast != null) {
				
				when(type) {
					1 -> {
						// キャンセルして再利用
						toast.cancel()
						toast.setText(text)
						toast.duration = duration
					}
					
					2 -> {
						// キャンセルせずに再利用
						toast.setText(text)
						toast.duration = duration
					}
					
					3 -> {
						// キャンセルして新しく作成
						toast.cancel()
						toast = Toast.makeText(this, text, duration)
						lastToast = toast
					}
					
					4 -> {
						// キャンセルせず新しく作成
						toast = Toast.makeText(this, text, duration)
						lastToast = toast
					}
				}
				toast?.setGravity(gravity, 0, 0)
				toast?.show()
			} else {
				toast = Toast.makeText(this, text, duration)
				lastToast = toast
				toast.setGravity(gravity, 0, 0)
				toast.show()
			}
			handler.removeCallbacks(toastRemover)
			handler.postDelayed(toastRemover, 4000L)
			btnCancel.isEnabled = true
		} catch(ex : Throwable) {
			Log.e(LOGTAG, "showToast failed.", ex)
		}
	}
}

ToastCompat

API level 25 以上で BadTokenException が出る不具合は ToastCompat https://github.com/drakeet/ToastCompat を使うと改善する。