プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

【Android】自動的に消えるボタン

『ちょっとだけ立体風地図ビューワ』で、ユーザーに操作を行ってもらうボタン類を、Google Mapの拡大縮小ボタンのように画面上に表示して、操作がなければ自動的に消したいと思ったので、自動的に消えるボタンを作ってみました。

1.考え方

自動的に消えるボタンをどのように使うかによって、実現方法は変わってくると思います。

今回は、次のような用途で使うことを前提に作りました。

  • FrameLayoutを使い、地図と自動的に消えるボタンを重ねておく(最初ボタンは見えなくする)
  • 画面にタッチすることで、ボタンを表示する
  • ボタンに対して操作がなければ、自動的に消える
  • ボタンに対して操作があれば、表示したままにする
  • ボタンに対する操作が終われば、一定時間後自動的に消える

表示の切り替えは、Viewのvisibilityを切り替えることで行うことにします。

また、一定時間たってから表示を消すために、タイマーを使うことにします。

こうすることで、望む振る舞いをするボタンを作ることができるのではないかと考えました。

 

ちなみに、最初は自動的に消えるボタン(ImageButtonを拡張したクラス)を作ったのですが、いくつものボタンを同じように表示したり・消したりする必要がでてきたので、最終的には自動的に消えるLinearLayoutにしました。この自動的に消えるLinearLayout上に普通のボタンを配置しておけば、LinearLayoutが消えるときにボタンなどもあわせて表示されなくなる、という寸法です。

以下のサンプルコードもLinearLayoutについて書いていますが、LinearLayoutが気にいらなければ、ボタンなど、ご自分の使いたいwidgetを継承すればよいと思います。

2. 自動的に消える仕掛け

上でも書いたように、自動的に消えるようにする仕掛けとして、まっさきに思いついたのがタイマーです。ボタン(実際にはLinearLayoutを拡張したクラス)を表示後、一定時間後にvisibilityがGONEやINVISIBLEになれば、自動的に消えたように見えるはずです。

まず最初に、既存のLinearLayoutを継承したViewクラスを作っておきます。LinearLayoutとしての振る舞いは特に変更しないので、コンストラクタだけオーバーライドして、メンバ変数等を初期化するようにしておきます。以下に初期化メソッドを示しておきます。

 

private void init() {
    mHandler = null;
    mTimer = null;
    mTask = null;
    mAnimFadein = null;
    mAnimFadeout = null;
    mLinearLayout = this;
}

 

次に、androidで利用できるタイマーを調べてみると、java.util.Timerが使えるようなので、これを使いました。しかし、Timerは別スレッドで動作するようであり、しかもandroidの場合、別スレッドからUIを操作すると例外が発生します。これを回避するため、android.os.Handlerクラスを経由して、UI操作を行うようにします。

実装上は、autoInvisibleというpublicメソッドを追加し、以下の処理を行います。

 

mTask = new TimerTask() {
                
    @Override
    public void run() {
        mHandler.post( new Runnable() {
            public void run() {
                mLinearLayout.setVisibility(View.GONE);
            }
        });
    }                    
};
        
mTimer.schedule(mTask, DELAY_TIME);

 

(参考にしたサイト)

Android でTimer

Android で再開する Java プログラミング(2) - 図形の描画

 

上記のコード中の、mHandler, mTimerはメインスレッド(Activityクラス) で生成したものを、このクラスに追加したsetHandlerAndTimerメソッド経由で保持しているものです。

 

public void setHandlerAndTimer(android.os.Handler handler, Timer timer) {
    mHandler = handler;
    mTimer = timer;
}

ちなみに、コンストラクタで渡していないのは、このクラスをXMLのレイアウト定義で使用する際に問題になるのではないかと考えたためです(確認はしていません)。

 

また、このアプリケーションの場合、タイマーを利用する処理はすべて同じTimerオブジェクトで処理をするようにしています。このためTimerオブジェクトも外部から渡すようにしました。

 

Timerを使うには、TimerTaskを作成し、Timerオブジェクトのschduleメソッドに実施時間に関する値と一緒に 渡してやります。ここでは、指定した時間後に一度だけ実行するタスクとしています。もちろん、Timerオブジェクトそのものは指定されたタスクを一定間隔で定期的に繰り返すことも可能です(引数の異なるオーバーロードメソッドがあります)。

あとは、setVisibilityのメソッドをオーバーライドし、このクラスに対してsetVisibilityが呼ばれると、最後にautoInvisibleが呼ばれて、タイマーが仕掛けられるという動きをします。

 

@Override
public void setVisibility(int visibility) {
    ・・・・・・・
    super.setVisibility(visibility);
        
    if (visibility == View.VISIBLE) {
        this.autoInvisible();
    }
}

 

なお、TimerTaskをnewした後、mTaskというメンバ変数に保持しているのですが、これはタスクをキャンセルする別メソッドで使うためです。

 

public void stopAutoInvisible() {
    if (mTask != null) {
        mTask.cancel();
        mTimer.purge();
        mTask = null;
    }
}

 

具体的には、表示されたボタンを操作している間はボタンが消えないようにするために、ボタンに操作があった場合は、このメソッドが呼ばれ、設定済みのタスクをキャンセルします。

タスクのキャンセルについて、こちらのWebページにあるように、

Android java.util.Timerの罠

Timerオブジェクトそのもののcancelメソッドを使うと、Timerオブジェクト自体を再利用することができなくなってしまいます。

このため、上記のようにメンバー変数に現在設定済みのタスクを保持しておき、必要に応じてTimerTaskのcancel()メソッドによりタスクのキャンセルを行いました(下記のサイトを参考にしました)。

Timerを使う。周期的に実行したり、一定時間後に実行したり

 

あと、androidのHandlerまわりの動作がよくわからなかったので戸惑ったのですが、こちらのWebページ、

Android のHandlerとは何か?

の説明が非常にわかりやすくて参考になりました。ご興味があれば一読されることをお勧めします。

3.スムーズに消える仕掛け

さて、上記のようにすれば、自動的に消えるのですが、パッと現れたり、消えたりするので、いまひとつな感じがします。やはり、Google Mapの拡大縮小ボタンのようにスムーズに現れて、スムーズに消えたほうが印象がいいと思います。

これは、アニメーションを使うことで実現することにしました。

アニメーションは、リソースとして定義します。たとえば、このような設定にしました。

 

表示されるときに使用するアニメーション定義(fadein.xml)

 

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fromAlpha="0"
    android:toAlpha="1"
    >
</alpha>

 

表示が消えるときに使用するアニメーション定義(fadeout.xml)

 

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fromAlpha="1"
    android:toAlpha="0"
    >
</alpha>

 

この自動的に消えるクラスには、表示用アニメーションリソースの設定メソッドと非表示用アニメーションリソースの設定メソッド(ソースコードは最後に示します)を用意しておき、どのようなアニメーションを使うかは、呼び出し側で自由に指定できるようにします。もちろん、指定がなければ、アニメーションを使わずに表示・非表示を切り替えます。

たとえば、このクラスを使う側では、

 

・・・・・・・    
mButtonLayout.setHandlerAndTimer(mHandler, mTimer);
        
Animation amin = AnimationUtils.loadAnimation(this, R.anim.fadein);
mButtonLayout.setFadeinAnimation(amin);
amin = AnimationUtils.loadAnimation(this, R.anim.fadeout);
mButtonLayout.setFadeoutAnimation(amin);
        
mButtonLayout.setVisibility(View.GONE);

 

のように設定します。mButtonLayoutがここで作成した自動的に消えるLinearLayoutクラスのオブジェクトです。このコードは、ActivityのOnCreate内で実行されます。

 

4. ソースコード

自動的に消えるwidgetクラスのソースコードを示しておきます。このソースは、『ちょっとだけ立体風地図ビューワ』で使っているものですので、使い方などはそちらのソースコードをご参考にしてもらえ ればと思います。

 

package com.mori_soft.android.perspectivemapviewer;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.widget.LinearLayout;

/**
 * 自動で表示されなくなるLinearLayout
 */
public class AutoInvisibleLinearLayout extends LinearLayout {

    private android.os.Handler mHandler;
    private Timer mTimer;
    private TimerTask mTask;
    private LinearLayout mLinearLayout;
    
    private long DELAY_TIME = 3000;    // 単位 ms

    private Animation mAnimFadein;
    private Animation mAnimFadeout;
    
    public AutoInvisibleLinearLayout(Context context) {
        super(context);
        init();
    }

    public AutoInvisibleLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    private void init() {
        mHandler = null;
        mTimer = null;
        mTask = null;
        mAnimFadein = null;
        mAnimFadeout = null;
        mLinearLayout = this;
    }

    public void setHandlerAndTimer(android.os.Handler handler, Timer timer) {
        mHandler = handler;
        mTimer = timer;
    }
    
    public void setFadeinAnimation(Animation amin) {
        mAnimFadein = amin;
    }
    public void setFadeoutAnimation(Animation amin) {
        mAnimFadeout = amin;
    }
    
    @Override
    public void setVisibility(int visibility) {
        // アニメーションの実行
        //   指定がある場合のみ実行する
        //   連続して呼ばれてもちらつかないようにガードをかけておく
        if (visibility == View.VISIBLE) { 
             if (mAnimFadein != null && (this.getVisibility() == View.GONE ||
                                                     this.getVisibility() == View.INVISIBLE)) {
                mLinearLayout.startAnimation(mAnimFadein);
            }
        } else {
            if (mAnimFadeout != null && (this.getVisibility() == View.VISIBLE)) {
                mLinearLayout.startAnimation(mAnimFadeout);
            }
        }
        super.setVisibility(visibility);
        
        if (visibility == View.VISIBLE) {
            this.autoInvisible();
        }
    }

    // 自動で見せなくするメソッド
    //   連続で呼ばれてもよいように既存のタスクのキャンセル処理も行う
    public void autoInvisible() {
        stopAutoInvisible();
        
        mTask = new TimerTask() {
                
            @Override
            public void run() {
                mHandler.post( new Runnable() {
                    public void run() {
                        mLinearLayout.setVisibility(View.GONE);
                    }
                });
            }                    
        };
        
        mTimer.schedule(mTask, DELAY_TIME);
    }

    // autoinvisibleを停止して、常に表示する
    public void stopAutoInvisible() {
        if (mTask != null) {
            mTask.cancel();
            mTimer.purge();
            mTask = null;
        }
    }

}

上記の文章では触れていない、連続でautoInvisibleが呼ばれた際のガードや、アニメーション動作用のガードなども入っていますが、難しいことではないので読めばわかるかと思います。

 

消えるまでの時間を内部で固定で持っていたりするので、いろいろと改良の余地はあると思いますが、ご参考になれば幸いです。