AndroidのView表示アニメーション

半透明のActivityを下からスライドさせて開く、というのをやりたくなった。
Activityのアニメーションで検索すると、大体ActivityのテーマのwindowAnimationStyleを設定する方法が出てくるようだ。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MyTheme" parent="android:Theme">   
		<item name="android:windowAnimationStyle">@style/Animation.MyAnimation</item> 
    </style>
</resources>

しかし、この方法はランチャーから起動する時など無視されることが多々ある。自分はランチャーから起動したときも下からせりあがってきて欲しいので、今回の件には使えない。

幸い、現在開発中のアプリは透明のActivityに半透明のviewを貼りつける構成になっているので、以下の方針を立てた。

  • Activityが完全に透明であれば、windowAnimationStyleの影響を受けない。
  • ルートのviewを最初は非表示にしておき、Activityの起動後にアニメーション表示させる。

やってみた

あらかじめルートviewのvisibilityをinvisibleに設定しておき非表示にする。

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

そしてアニメーションを定義。
anim/open_main_screen.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
	android:fromYDelta="100%"
	android:toYDelta="0%"
	android:duration="200"
	android:fillAfter="true"
	>
</translate>

translateは直線移動のアニメーション。fromYDelta、toYDeltaで開始/終了Y座標(絶対座標でも%でも可)を、durationでアニメーションにかける時間(msec)を設定する。さらにfillAfterでアニメーション終了後の状態を維持する。


仕上げにjavaのコードから呼び出し。

Animation animation = AnimationUtils.loadAnimation(activity,R.anim.open_main_screen);
View rootView = activity.findViewById(R.id.main);
rootView.startAnimation(animation);

思った通りに動作した。Acitivtyが閉じるときにルートviewを非表示にしておけば完璧。

おまけ

XMLを使いたくなければ全てJavaで記述することもできる。

TranslateAnimation animation = 
        new TranslateAnimation(
    		Animation.RELATIVE_TO_PARENT, 0.0f,
    		Animation.RELATIVE_TO_PARENT, 0.0f, 
    		Animation.RELATIVE_TO_PARENT, 1.0f,
    		Animation.RELATIVE_TO_PARENT, 0.0f);
animation.setDuration(200);
animation.setFillAfter(true);
rootView.startAnimation(animation);

Animation.RELATIVE_TO_PARENTを指定すればXMLのときと同じく%で指定できる。(1.0 == 100%)

アプリの再インストールを検知する方法

アプリのインストール情報を検知するためandroid.manufestに以下の定義をする。

<receiver android:name=".MyReciever">
    <intent-filter>
        <action android:name="android.intent.action.PACKAGE_ADDED" />
        <action android:name="android.intent.action.PACKAGE_CHANGED" />
        <action android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
        <action android:name="android.intent.action.PACKAGE_INSTALL" />
        <action android:name="android.intent.action.PACKAGE_REMOVED" />
        <action android:name="android.intent.action.PACKAGE_REPLACED" />
        <action android:name="android.intent.action.PACKAGE_RESTARTED" />
    </intent-filter>
</receiver>

そしてjavaのコードでレシーバーを定義する。

public class MyrReciever extends BroadcastReceiver {

	@Override
	public void onReceive(Context context, Intent intent) {
		String action = intent.getAction();
		Log.d("MyrReciever" ,action);
	}
}

しかしこれだけではインテントが飛んでこない。
調べてみるとandroid deveopersに回答があった。
どうやらインテントフィルターにschemeを追加する必要があるらしい。
つまり、android.manufestを以下のように書き換える。

<receiver android:name=".MyReciever">
    <intent-filter>
        <action android:name="android.intent.action.PACKAGE_ADDED" />
        <action android:name="android.intent.action.PACKAGE_CHANGED" />
        <action android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
        <action android:name="android.intent.action.PACKAGE_INSTALL" />
        <action android:name="android.intent.action.PACKAGE_REMOVED" />
        <action android:name="android.intent.action.PACKAGE_REPLACED" />
        <action android:name="android.intent.action.PACKAGE_RESTARTED" />
        <data android:scheme="package"/>
    </intent-filter>
</receiver>

これでパッケージの状態が変化するとブロードキャストレシーバーにインテントが飛んでくるようになる。
ちなみにdataタグにpathを指定することで変更を通知するパッケージを絞ることができるようだ。

2012/2/9
通知するパッケージを絞れるというのは勘違いのようでした。
これらのブロードキャストのUriは"package:com.example.myapp"というURIだか何なんだかよく分からない形式になっていて、どうもintent-filterで絞りをかける手段はないようです。

<data android:scheme="package" android:path="com.example.myapp" />

参考:
Google グループ

Activityの連続起動を検知する

事の発端
自作のアプリケーションランチャーでHomeボタンの押下を検知したくなった。
Homeボタンを一回押したらランチャーを表示し、ランチャーが表示されている状態でHomeボタンを押したら別の外部のHomeアプリを起動したい(PreHomeみたいな動作)。


というわけでwebで調査していたところ、日本androidの会にActivity#onUserLeaveHintをオーバーライドすればHomeボタンの検知ができるとの情報があった。
しかしAPIリファレンスを読んでも、記述があいまいと言うか、いまいち要領を得ない。


試してみた
ActivityLifeCycleというテストアクティビティをつくってlogをトレースしてみる。


(1)Homeボタン押下(HomeApp起動)

ActivityLifeCycle(10487): onUserInteraction
ActivityLifeCycle(10487): onUserLeaveHint
ActivityLifeCycle(10487): onPause
ActivityManager(120): Displayed activity com.example.android.home/.HomeApp: 285 ms (total 285 ms)


(2)Homeボタン長押し (アプリ履歴表示)

dalvikvm(204): GC_EXPLICIT freed 3053 objects / 178712 bytes in 79ms
dalvikvm(120): GC_EXTERNAL_ALLOC freed 21870 objects / 973168 bytes in 105ms
StatusBar(120): DISABLE_EXPAND: yes
  • > 自Activity(ActivityLifeCycle )起動
ActivityManager(120): Starting activity: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10100000 cmp=com.amle.android.mysample/.ActivityLifeCycle }


(3)Homeボタン長押し(アプリ履歴表示)

dalvikvm(120): GC_EXTERNAL_ALLOC freed 19860 objects / 988712 bytes in 104ms
StatusBar(120): DISABLE_EXPAND: yes

-> 他アプリ(smartbar/.GridFavouristApps)起動

ActivityManager(120): Starting activity: Intent { flg=0x10100000 pkg=com.aps.smartbar cmp=com.aps.smartbar/.GridFavouristApps }
ActivityLifeCycle(10487): onUserInteraction
ActivityLifeCycle(10487): onUserLeaveHint
ActivityLifeCycle(10487): onPause
AudioFlinger(90): write blocked for 163 msecs, 2069 delayed writes, thread 0xdae0
ActivityManager(120): Displayed activity com.aps.smartbar/.GridFavouristApps: 421 ms (total 421 ms)


(4)検索ボタン長押し(他アプリ(smartbar/.GridFavouristApps)起動)

ActivityLifeCycle(10257): onUserInteraction
ActivityLifeCycle(10257): onUserInteraction
ActivityManager(120): Starting activity: Intent { act=android.intent.action.SEARCH_LONG_PRESS flg=0x10000000 cmp=com.aps.smartbar/.SearchButtonClickedReceiver }
ActivityLifeCycle(10257): onUserInteraction
ActivityLifeCycle(10257): onUserLeaveHint
ActivityLifeCycle(10257): onPause
ActivityLifeCycle(10257): onUserInteraction
InputManagerService(120): Window already focused, ignoring focus gain of: com.android.internal.view.IInputMethodClient$Stub$Proxy@445bd798
ActivityLifeCycle(10257): onUserInteraction
ActivityManager(120): Starting activity: Intent { flg=0x10000000 pkg=com.aps.smartbar cmp=com.aps.smartbar/.GridFavouristApps }


(5)ステータスバーを開く

(反応なし)
  • >他アプリ(MobileRTM/.Activities.RTMReminderActivity)起動
ActivityManager(120): Starting activity: Intent { flg=0x10000000 cmp=com.rememberthemilk.MobileRTM/.Activities.RTMReminderActivity bnds=[0,568][480,664] (has extras) }
ActivityLifeCycle(10487): onUserInteraction
ActivityLifeCycle(10487): onUserLeaveHint
ActivityLifeCycle(10487): onPause
AudioFlinger(90): write blocked for 174 msecs, 2072 delayed writes, thread 0xdae0
ActivityManager(120): Displayed activity com.rememberthemilk.MobileRTM/.Activities.RTMReminderActivity: 186 ms (total 186 ms)


(6)自Activityの表示中に外部のアラームアプリを使ってAlarmManager経由で他アプリ(myalarm/.TimeupActivity)を起動させる。

ActivityManager(52): Starting activity: Intent { act=android.intent.action.MAIN flg=0x10000000 cmp=com.amle.android.myalarm/.TimeupActivity (has extras) }
ActivityLifeCycle(1183): onUserInteraction
ActivityLifeCycle(1183): onUserLeaveHint
ActivityLifeCycle(1183): onPause
AlarmService(353): onStart


(7)自Activityで別スレッドを作り、10秒後にブラウザを起動。

ActivityManager(52): Starting activity: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.android.browser/.BrowserActivity }
ActivityLifeCycle(1146): onUserInteraction
ActivityLifeCycle(1146): onUserLeaveHint
ActivityLifeCycle(1146): onPause


(8)自Activityで別スレッドを作り、10秒後に自Activityをfinish()する。

ActivityLifeCycle(1240): onPause


(9)自Activityで別スレッドを作り、10秒後に自アプリの別activity(MySample)を起動する。

dalvikvm(204): GC freed 117 objects / 5256 bytes in 66ms
ActivityManager(52): Starting activity: Intent { cmp=com.amle.android.mysample/.MySample }
ActivityLifeCycle(626): onUserInteraction
ActivityLifeCycle(626): onUserLeaveHint
ActivityLifeCycle(626): onPause


(10)自Activityで別スレッドを作り、10秒後に自アプリの別activity(MySample)を起動し、その後自分をfinish()する。

dalvikvm(544): GC freed 2963 objects / 314176 bytes in 87ms
ActivityManager(52): Starting activity: Intent { cmp=com.amle.android.mysample/.MySample }
ActivityLifeCycle(662): onUserInteraction
ActivityLifeCycle(662): onUserLeaveHint
ActivityLifeCycle(662): onPause

いろいろなパターンを試してみたが、まとめると

  • 他のActivityを起動させるとonUserLeaveHint()が呼ばれる。(Homeキー、検索キー長押し、バックグラウンドスレッドからのstartActivity()やAlarmManagerからのブロードキャストなど手段を問わない。)
  • 自アプリ内の他のactivityを起動させても呼ばれる。
  • 自Activityを起動させても呼ばれない。
  • 自Activityをfinish()させても呼ばれない。(ただし他のActivityを起動した後にfinish()してもonUserLeaveHint()は呼ばれる。)
  • ステータスバーの開閉、標準のアプリ履歴の表示では呼ばれない。(そこから別のActivityを起動すると呼ばれる。)

…となる。


実際のところonUserLeaveHint()は、他activityが起動される時には、一部の例外(着呼のときには呼ばれない)を除いてほとんど常に呼ばれると思ったほうが良さそうだ。


結論:
onUserLeaveHint()は、他のactivityが起動された(startActivity()された)ことを検知するためのメソッドであると考えられる。
最初はonPause()との使い分けがわからなかったが、「電話の着信では呼ばれない」、「自activityをfinish()しても呼ばれない」という2点でonPause()と異なる。


結局のところHomeボタンの検知に使えるわけではなさそうだが2重起動の検知には使えそうだ。
つまり、onUserLeaveHint()を挟むことなく連続でonResume()が呼び出されたら、それは連続起動(ランチャーActivityの表示中にさらにランチャーActivityが呼び出された)である。


着呼の時にはonUserLeaveHint()が呼び出されないが、この時はActivityが終了するようなので連続起動扱いにはならない。
つまり、着呼の後には常にランチャーが表示されることになる。
この動作が正しいかどうかはアプリの仕様次第だろう。


参考:
Google グループ
Activity  |  Android Developers