2012年5月30日水曜日

AndroidでToastの表示時間を自由に設定したりアニメーションなしで即非表示したい!

Toastの問題点

AndroidにはToastという便利なコポーネントがある。しかし、Toastにはちと問題がある。

1. 表示時間を自由に設定できない
Toast.setDuration()があるものの、これが効かない。どうやらLENGTH_LONGかLENGTH_SHORTしか受け付けないらしい。


2. アニメーションなしで即非表示に出来ない
Toast.cancel()があるものの、どうしてもフェードアウトのアニメーションがついてしまって即非表示にできない。

3.アニメーションを変更する方法が見つからない
ならアニメーションを変更して即非表示に近いところまで持って行こうかと思うが、そのアニメーションを変更する方法が見つからない。

これらは恐らくどうしようもないのだろうと思って、PopupWindowを使ってこれらを実現しながらToastのように使えるコンポーネントを作ることにした。


ようするに、こんな風にしたい!



Eclipseプロジェクトファイル:ToastCustom.zip(155KB)


実装

ToastCustom.java
package jp.example;

import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.PopupWindow;


public class ToastCustom extends PopupWindow implements Runnable {


  // ユーザー定義アニメーションスタイル
  protected int userDefinedAnimationStyle = -1;

  // 自動非表示するまでの時間
  protected int duration = 1000;

  // 時間差で非表示メソッドを起動する
  protected Handler handler = new Handler();


  public ToastCustom(View contentView, int width, int height) {
    super(contentView, width, height);
  }


  @Override
  public void run() {
    Log.v("run", "自動非表示");
    dismiss();
  }


  @Override
  public void setAnimationStyle(int animationStyle) {
    //dismissNow()用にユーザ定義のアニメーションスタイルを保存しておく
    userDefinedAnimationStyle = animationStyle;
    super.setAnimationStyle(animationStyle);
  }


  @Override
  public void showAtLocation(View parent, int gravity, int x, int y) {
    Log.v("showAtLocation()", "表示");
    //dismissNow()でアニメーションを無効にした場合に再設定する
    if (userDefinedAnimationStyle != -1) {
      super.setAnimationStyle(userDefinedAnimationStyle);
    }
    if (duration > 0) { // durationが-1なら自動非表示しない
      handler.postDelayed(this, duration);
    }
    super.showAtLocation(parent, gravity, x, y);
  }


  @Override
  public void dismiss() {
    Log.v("dismiss()", "非表示");
    handler.removeCallbacks(this); // 自動非表示を無効にする
    super.dismiss();
  }


  /**
   * アニメーションも無視して直ちに非表示にする
   */
  public void dismissNow() {
    Log.v("dismissNow()", "ただちに非表示");
    super.setAnimationStyle(-1);
    update(); //これをしないとsuper.setAnimationStyle(-1);の効果が次のshow...まで反映されない
    dismiss();
  }


  /**
   * @return 自動非表示されるまでの時間(アニメーション時間含まず)
   */
  public int getDuration() {
    return duration;
  }


  /**
   * @param duration 自動非表示されるまでの時間(アニメーション時間含まず)。-1で自動非表示無効。
   */
  public void setDuration(int duration) {
    this.duration = duration;
  }


} // END class ToastCustom

POINT 1
Handlerを使って指定の時間差で非表示にしているところ。RunnableをimplementsしてHandlerに自分自身を渡すようにした。

POINT 2
dismissNow()のところ。一時的にアニメーションを無効にした上でdismiss()で非表示にしている。setAnimationStyle()した直後にupdate()を呼んでいるところ重要。

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parent"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
</LinearLayout>

idを設定しているだけでほとんど空っぽ。

res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="toast_animation">
        <item name="android:windowEnterAnimation">@android:anim/fade_in</item>
        <item name="android:windowExitAnimation">@android:anim/fade_out</item>
    </style>
</resources>

アニメーションスタイルの設定。今回はAndroidにビルトインされているfade_inとfade_outを使ったけど、これはもちろん自分で作ったアニメーションを設定できる。スライドするトーストなんかもできるだろう。

ToastCustomActivity.java
package jp.example;

import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;


public class ToastCustomActivity extends Activity {


  protected TextView tv;
  protected ToastCustom toast;
  protected View parent;


  @Override
  public void onCreate(Bundle savedInstanceState) {

    // お約束
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Viewのトップ
    parent = findViewById(R.id.parent);

    // トーストの中味
    tv = new TextView(this);
    tv.setBackgroundColor(0xFFFF0000);
    tv.setTextColor(0xFFFFFFFF);
    tv.setWidth(160);
    tv.setGravity(Gravity.CENTER);
    tv.setPadding(10, 10, 10, 10);
    WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
    lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
    tv.setLayoutParams(lp);

    // トースト作成
    toast = new ToastCustom(tv, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
    toast.setAnimationStyle(R.style.toast_animation); // アニメーションはスタイルで設定する
    toast.setDuration(1000); // 1000msecで自動非表示
    toast.setClippingEnabled(false); // 画面外にはみ出してもいいことにする

  }


  @Override
  public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        tv.setText("TouchDown! rawX=" + (int) event.getRawX() + " rawY=" + (int) event.getRawY());
        toast.showAtLocation(parent, Gravity.TOP | Gravity.LEFT, (int) event.getRawX(), (int) event.getRawY()); //トースト表示
        break;
      case MotionEvent.ACTION_UP:
        toast.dismissNow(); //トーストを速攻で非表示
        break;
    }
    return super.onTouchEvent(event);
  }


} // END class ToastCustomActivity

トーストの中味はシンプルにTextViewだけにした。もちろん、もっと複雑でかっくいいViewにすることもできる。

showAtLocation()で親となるViewを渡してあげないといけないところが面倒。NULL渡しもダメだから困る(*´Д`)

タッチダウンでクリックした場所にトーストを表示。タッチアップで即非表示。

setDuration()で-1を渡しておくと自動非表示が無効化されて、タッチダウンしている間ずっと表示できるようになる。

まとめ

Toastというよりツールチップに近いけど、Androidにはマウスオーバーはないからツールチップは存在し得ないので、Toastってことにしておこう。

調べてみてもあまり有効な解決策がなく、かなりの方がお困りのようだったので、そういった方の参考になれば幸いです。