Androidの最近のブログ記事

Android Advent Calendar 2012 12月12日(表)担当の adakoda です。

本エントリでは、Android SDK Tools Rev.21 でサポートされた新しい UI テストフレームワークである uiautomator を使用し、

Android アプリケーションの UI テストケースを作成、実行する方法について解説し、

サンプルテストコードとして 「Android 標準の電卓アプリ上で足し算の結果を確認する」 テストケースを作成します。

※本エントリの内容は、「Android SDK Tools, Revision 21(21.0.1 rc1)」  「Android SDK Platform, API 17 (Android 4.2)」を対象とした内容となっています

 

uiautomator ツール

◆ テストプロジェクトを作成する

Eclipse 上で新規に「Java Project」を作成し、JUnit3、uiautomator.jar、android.jar ライブラリを追加することで作成できます。

【手順】

1. Eclipse を起動し、[File] > [New] > [Java Project] をクリックする。

2. [Create a Java Project] 画面で、[Project name] に作成したいプロジェクト名を入力し、[Next] をクリック。

3. [Java Settings] 画面、[Libraries] タブで [Add Library...] をクリック、

    [JUnit] が選択された状態で [Next] をクリック、[JUnit3] が選択された状態で [Finish] をクリック。

4. 同 [Java Settings] 画面、[Libraries] タブで [Add External JARs...] をクリック、

    インストールした Android SDK ディレクトリの platforms\android-17 下にある android.jar、uiautomator.jar を選択。

5. [Finish] をクリックすると、作成完了。

 

◆ テストクラスを作成する

UiAutomatorTestCase クラスを継承したクラスを作成し、テストメソッドを作成します。

※UiAutomatorTestCase クラスは、junit.framework.TestCase を継承したクラス

※具体的なテストクラスは、後述するサンプルテストコードで解説します

 

◆ ant を使用してビルドする

作成したプロジェクトを ant を使用してビルドするには、android コマンドを使用し、ant ビルドに必要なファイルを作成し、ビルドします。

【手順】

1. コマンドプロンプト/ターミナル上で 「android create uitest-project -n <name> -t <target> -p <path>」を実行する。

  ※<name> には、プロジェクト名 を指定する

  ※<target> には、「android list target」で表示される対象 API の Target ID を指定する

  ※<path> には、対象プロジェクトのパス を指定する

  例) 「android create uitest-project -n UIAutomatorSample -t 20 -p .」

2. プロジェクト下に「build.xml」ファイルが生成されていることを確認し、同ディレクトリで「ant build」を実行する。

 

◆ ビルドしたファイルを使用してテストを実行する

ビルドしてできた jar ファイルを adb コマンドで Android 端末に転送し、uiautomator ツールを使用してテストを実行します。

【手順】

1. 「adb push <ビルドして生成されたjarファイル> <転送先ディレクトリ>」を実行し、jar ファイルを Android 端末に転送します。

  例) 「adb push .\bin\UIAutomatorSample.jar /data/local/tmp」

2. 「adb shell uiautomator runtest <ビルドして生成されたjarファイル> -c <テストクラス>」を実行し、テストを実行する。

  例) 「adb shell uiautomator runtest UIAutomatorSample.jar  -c com.adakoda.android.uiautomatorsample.CalculatorTest」

 

◆ サンプルテストケース

ここでは、Android 標準の電卓アプリ上で足し算の結果を確認するテストケースを作成します。

テストのシナリオは、以下のとおりです。

【テストのシナリオ】

1. ホームボタンを押してアプリケーションランチャーを表示する。

2. アプリケーションランチャーから電卓アプリを起動する。

3. 電卓アプリの消去ボタンを長押しして、結果をクリアする。

4. 電卓アプリで「 1 」「 + 」「 2 」「 = 」ボタンをクリックし、結果が「 3 」であることを確認する。

 

◆ サンプルテストコード

package com.adakoda.android.uiautomatorsample;

import android.widget.TextView;

import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;

public class CalculatorTest extends UiAutomatorTestCase {

    public void testAddition() throws UiObjectNotFoundException {
        // 1. ホームボタンを押してアプリケーションランチャーを表示する。
        getUiDevice().pressHome();

        // 2. アプリケーションランチャーから電卓アプリを起動する。
        UiObject allAppsButton = new UiObject(new UiSelector().description("アプリ"));
        allAppsButton.clickAndWaitForNewWindow();
        UiObject appsTab = new UiObject(new UiSelector().text("アプリ"));
        appsTab.click();

        // スワイプしながらアイテムを検索できるUiScrollableインスタンスを作成
        UiScrollable appViews = new UiScrollable(new UiSelector().scrollable(true));
        // スワイプ時のスクロール方向を水平方向に設定
        appViews.setAsHorizontalList();

        // アプリランチャーの子ビューから以下の条件を満たすUIオブジェクトを取得
        // 「クラス名が"android.widget.TextView"」、かつ、「テキストが"電卓"」
        UiObject targetApp = appViews.getChildByText(
                new UiSelector().className(TextView.class.getName()),
                "電卓");
        // 見つかったUIオブジェクト(ここでは電卓アプリアイコン)をクリックする
        targetApp.clickAndWaitForNewWindow();

        // 指定したパッケージ名のアプリの存在を確認する
        UiObject appValidation = new UiObject(
                new UiSelector().packageName("com.android.calculator2"));
        assertTrue(appValidation.exists());

        // 3.電卓アプリの"削除"ボタンを長押しして、結果をクリアする。
        UiObject buttonClear = new UiObject(new UiSelector().text("削除"));
        buttonClear.longClick(); // 長押し

        // 4. 電卓アプリで「 1 」「 + 」「 2 」「 = 」ボタンをクリックし、結果が「 3 」であることを確認する。

        // "1"ボタンを押す
        UiObject button1 = new UiObject(new UiSelector().text("1"));
        button1.click();
        // "+"ボタンを押す
        UiObject buttonPlus = new UiObject(new UiSelector().text("+"));
        buttonPlus.click();
        // "2"ボタンを押す
        UiObject button2 = new UiObject(new UiSelector().text("2"));
        button2.click();
        // "="ボタンを押す
        UiObject buttonEqual = new UiObject(new UiSelector().text("="));
        buttonEqual.click();

        // エディットテキストの値を取得する
        UiObject editText = new UiObject(new UiSelector().className("android.widget.EditText"));
        String textEditText = editText.getText();

        // 結果を確認する
        assertEquals(String.valueOf("3"), textEditText);
    }

}

 

◆ テスト実行結果(画面遷移)

上記テストコードを実行すると、以下の画面が自動的に遷移します。

home.png

0. テストを実行する前のホーム画面。

 

app1.png

1. ホームボタンを押し、アプリケーションランチャーが起動した画面。

 

app2.png

2. 自動的に水平スワイプが実行され、"電卓"というテキストのビューを探している画面。

 

calc1.png

3. 電卓アプリを起動し、削除ボタンを長押ししている画面。

 

calc2.pngcalc3.png

4. 「1」「+」「2」「=」ボタンを押して、「3」という結果となっていることを確認している画面。

 

uiautomatorviewer ツール

◆ UI コンポーネントのスキャンと解析を行う

uiautomator では、UiSelector クラスを使用して操作対象の UI を特定するのですが、

このための UI 解析ツールとして、uiautomatorviewer ツールが提供されています。

ツールを起動し、画面キャプチャ用のアイコン(左から2番目)を押すと、解析結果が表示され、

任意のビューの情報を表示させることができます。

 ※Windows 版は、Rev21.の bat ファイルに不具合があるため、21.0.1 rc1 でないと正常に動作しません

uiautomatorviewer.png

 

おわりに

uiautomator テストフレームワークは、まだまだ開発中のツールと思われるため、

本エントリで紹介した内容は、将来変更される可能性があります。

特に、ant ビルドの部分は、uibuild.xml ファイル内に <!-- todo: fix this --> と書かれているとおり、

ant test コマンドなどでもっと簡単にテストを実行できるようになると思われます^^;

 

◆ 参考リソース

 

◆ あわせて読みたい(Android Advent Calendar 2012 のテスト関連のエントリ)

先週くらいに、Twitter の TL 上で見たこともない TOEIC の点数が流れていましたので^^;

西野さんから献本していただいた書籍をご紹介。

 

『アプリケーションをつくる英語』

  • 発売日:2012/09/21発売
  • ページ数:328P
  • サイズ・判型:B5変型判
  • 著者:西野 竜太郎 著
  • ISBNコード:978-4-8443-3284-8
  • 定価:2,400円+税
  • URL:http://www.impressjapan.jp/books/3284

 

内容

  • 第1部 よく使う単語

辞書形式で単語の意味や例文、解説が書かれています。

通常の辞書と違って、エラーやヘルプなど、アプリ開発で必ず使う単語が、

分類別(1章から16章)にまとめられているので、効率的に探すことができます。

 

  • 第2部 構文パターン

「できます(可能)」「できません(不可能)」など、よく使う構文(※)の例文、解説が書かれています。

※「可能」「不可能」「可能性」「完了」「未完了」「成功・失敗」「現在」「予告」
 「指示」「推奨」「禁止」「質問・確認」「条件」「目的」「理由「」有無」「提示」

アプリケーションは、必ず何かしらのメッセージを表示することになるのですが、

ほとんどのものは、ここに書かれている構文で表現できるんじゃないかと思います^^

 

  • 第3部 英語ライティングとグローバリゼーション

基本的な英文法の解説から始まり、

「エリプシス」「キャプタリゼーション」などのUIメッセージの書き方、

アプリケーションを国際化する際の注意点、UI関連のガイドラインへのリンクなど、役に立つ資料が書かれています。

アプリケーション開発者には是非読んでほしい内容だと思います。

 

電子書籍版

電子書籍版は、達人出版会から購入できます。

http://tatsu-zine.com/books/english4app 

 「まず電書で、次に紙で」 という流れはいいですね。

まさに希望の光。

 

最後に

最近では、様々なプラットフォーム上で、開発したアプリケーションを簡単に全世界に公開できるようになっており、

英語対応は必須になっています。

本書は、まさに、そんな私たち開発者にとって必須の本だと思います。

開発者の時給を考えればなおさら。。。

 

 ところで、この値段で、ここまでノウハウを出して大丈夫なの^^;

日本最大級の開発コンテスト「MA8」の連動イベント、信州MashupCaravan&Meetup with GDG信州でライブコーディングしてきました。

 

内容としては、持ち時間15分内に、協賛企業の(REST)APIを使用して制限時間内にAndroidアプリを作るというもの。

 

結果は・・・

 

アプリは何とか完成したものの、持ち時間3~5分オーバー、

途中で時間が足りないことに気づいていたので、少し機能も減らしました。。。><

 

想定外だったのが、立ってタイピングしたせいか、両腕が安定しなくて、タイポを連発しまくったこと。

緊張はしてなかったのですが、手元は正直で、まー、とにかくタイピングのぎこちないことwww

(全体の5分の2くらいで、投げ出して、3分間クッキングみたいに完成品を出そうかと思ったほど。)

 

今回自分でやってみて、あらためてライブコーディングすらすらできる人、すごいなぁ、と感じました。。。

それでも、前日に練習した時に1時間かかったものが、その3分の1くらいでできたので良かったのと、

最後、無事、端末上で1回で動作した時、暖かい拍手を頂けたのが本当に嬉しかったですm_ _m

(来てくれた方々がいい人で良かったわー)

(来てくれた方々がいい人で良かったわー)

しろめ

 

教訓

  • ライブコーディングする場合は、立って練習せよ
  • プロジェクタの関係で画面の作業エリアが狭くなるので、1024x768くらいで練習せよ
  • トラぶった時に、気の利いたトークができるように、トークも磨け
  • 前日に InkScape 起動して萌絵(↓)描いて現実逃避する暇があったら練習せよ

 

jcromtan2.png

ActionScript 用の有名なアニメーションライブラリである Tweener を Android で使用する方法について説明します。

 

Tweener を使用すると、

Tweener.to(imageView, 3000, "alpha", 0);

のような感じでプロパティアニメーションさせたり、

 

Tweener.to(imageView, // アニメーション対象
    3000, // アニメーション期間(単位はミリ秒)
    "translationX", 200.0f, // 変化させたいプロパティ名, 値
    "translationY", 100.0f, // 変化させたいプロパティ名, 値
    "ease", Ease.Quad.easeInOut, // 補間関数
    "onComplete", new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            // 必要であれば、アニメーション終了時のコードを書きます
        }
    });

のような感じで、複雑なアニメーション(複数プロパティ、補完関数変更、終了イベント補足)も、短いコードで書くこともできます。

 

Android 3.0 以降であれば、Android でもプロパティアニメーションがサポートされているため、

新たに Tweener を使用するまでもないのですが、Tweener の方が馴染みがある場合には、役に立つかもしれません。

ちなみにプロパティアニメーションについては『Androidプログラミング上達読本』にも解説があるので、興味を持った方は読んでみてください^^;

 

使い方

1. AOSP から Android 4.0 以降のソースコードをダウンロードする。

 ※必要なコードは frameworks 下の一部のコードだけなので repo ではなく、git で直接取得してもよいです

git clone https://android.googlesource.com/platform/frameworks/base

 

2. 取得したコードの中から以下のクラスをコピーし、任意のパッケージ名に変更する。

  • frameworks\base\core\java\com\android\internal\widget\multiwaveview\Tweener.java
  • frameworks\base\core\java\com\android\internal\widget\multiwaveview\Ease.java

 ※Tweener クラスはパッケージスコープになっているので必要に応じてアクセスレベルを変更してください

 ※パッケージ名を変更しないと、4.0(ICS)―4.1(JB)間で内部クラスが変更されているので上手く動作しない可能性があります

 

3. Tweener.to() を呼び出し、Tweener アニメーションコードを書く。

 ※変化させるプロパティ名は、ActionScript でなく、Adnroid で使用できるプロパティ名を書きます

 ※Tweener.from() などは移植されていないようです

 

Tweener

ActionScript は言語レベルでプロパティをサポートされているので、Tweener の実装は非常にシンプルなコードになっています。

 

Android 4.0 フレームワークの内部クラスで実装されている Tweener のコード

  • Twneer.java

Android のプロパティアニメーションAPI(PropertyValuesHolder、ObjectAnimator など)を使用して移植されています。

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.widget.multiwaveview;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

import android.animation.Animator.AnimatorListener;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.util.Log;

class Tweener {
    private static final String TAG = "Tweener";
    private static final boolean DEBUG = false;

    ObjectAnimator animator;
    private static HashMap<Object, Tweener> sTweens = new HashMap<Object, Tweener>();

    public Tweener(ObjectAnimator anim) {
        animator = anim;
    }

    private static void remove(Animator animator) {
        Iterator<Entry<Object, Tweener>> iter = sTweens.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<Object, Tweener> entry = iter.next();
            if (entry.getValue().animator == animator) {
                if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey())
                        + " sTweens.size() = " + sTweens.size());
                iter.remove();
                break; // an animator can only be attached to one object
            }
        }
    }

    public static Tweener to(Object object, long duration, Object... vars) {
        long delay = 0;
        AnimatorUpdateListener updateListener = null;
        AnimatorListener listener = null;
        TimeInterpolator interpolator = null;

        // Iterate through arguments and discover properties to animate
        ArrayList<PropertyValuesHolder> props = new ArrayList<PropertyValuesHolder>(vars.length/2);
        for (int i = 0; i < vars.length; i+=2) {
            if (!(vars[i] instanceof String)) {
                throw new IllegalArgumentException("Key must be a string: " + vars[i]);
            }
            String key = (String) vars[i];
            Object value = vars[i+1];
            if ("simultaneousTween".equals(key)) {
                // TODO
            } else if ("ease".equals(key)) {
                interpolator = (TimeInterpolator) value; // TODO: multiple interpolators?
            } else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) {
                updateListener = (AnimatorUpdateListener) value;
            } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) {
                listener = (AnimatorListener) value;
            } else if ("delay".equals(key)) {
                delay = ((Number) value).longValue();
            } else if ("syncWith".equals(key)) {
                // TODO
            } else if (value instanceof float[]) {
                props.add(PropertyValuesHolder.ofFloat(key,
                        ((float[])value)[0], ((float[])value)[1]));
            } else if (value instanceof Number) {
                float floatValue = ((Number)value).floatValue();
                props.add(PropertyValuesHolder.ofFloat(key, floatValue));
            } else {
                throw new IllegalArgumentException(
                        "Bad argument for key \"" + key + "\" with value " + value.getClass());
            }
        }

        // Re-use existing tween, if present
        Tweener tween = sTweens.get(object);
        ObjectAnimator anim = null;
        if (tween == null) {
            anim = ObjectAnimator.ofPropertyValuesHolder(object,
                    props.toArray(new PropertyValuesHolder[props.size()]));
            tween = new Tweener(anim);
            sTweens.put(object, tween);
            if (DEBUG) Log.v(TAG, "Added new Tweener " + tween);
        } else {
            anim = sTweens.get(object).animator;
            replace(props, object); // Cancel all animators for given object
        }

        if (interpolator != null) {
            anim.setInterpolator(interpolator);
        }

        // Update animation with properties discovered in loop above
        anim.setStartDelay(delay);
        anim.setDuration(duration);
        if (updateListener != null) {
            anim.removeAllUpdateListeners(); // There should be only one
            anim.addUpdateListener(updateListener);
        }
        if (listener != null) {
            anim.removeAllListeners(); // There should be only one.
            anim.addListener(listener);
        }
        anim.addListener(mCleanupListener);
        anim.start();

        return tween;
    }

    Tweener from(Object object, long duration, Object... vars) {
        // TODO:  for v of vars
        //            toVars[v] = object[v]
        //            object[v] = vars[v]
        return Tweener.to(object, duration, vars);
    }

    // Listener to watch for completed animations and remove them.
    private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() {

        @Override
        public void onAnimationEnd(Animator animation) {
            remove(animation);
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            remove(animation);
        }
    };

    public static void reset() {
        if (DEBUG) {
            Log.v(TAG, "Reset()");
            if (sTweens.size() > 0) {
                Log.v(TAG, "Cleaning up " + sTweens.size() + " animations");
            }
        }
        sTweens.clear();
    }

    private static void replace(ArrayList<PropertyValuesHolder> props, Object... args) {
        for (final Object killobject : args) {
            Tweener tween = sTweens.get(killobject);
            if (tween != null) {
                tween.animator.cancel();
                if (props != null) {
                    tween.animator.setValues(
                            props.toArray(new PropertyValuesHolder[props.size()]));
                } else {
                    sTweens.remove(tween);
                }
            }
        }
    }
}

 

  • Ease.java

Tweener で定義されているイージングを Android のプロパティアニメーションAPI を使用して移植してあります。

TimeInterpolator を継承したクラスなので、ここだけでも使えますね!

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.widget.multiwaveview;

import android.animation.TimeInterpolator;

class Ease {
    private static final float DOMAIN = 1.0f;
    private static final float DURATION = 1.0f;
    private static final float START = 0.0f;

    static class Linear {
        public static final TimeInterpolator easeNone = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return input;
            }
        };
    }

    static class Cubic {
        public static final TimeInterpolator easeIn = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return DOMAIN*(input/=DURATION)*input*input + START;
            }
        };
        public static final TimeInterpolator easeOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START;
            }
        };
        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return ((input/=DURATION/2) < 1.0f) ?
                        (DOMAIN/2*input*input*input + START)
                            : (DOMAIN/2*((input-=2)*input*input + 2) + START);
            }
        };
    }

    static class Quad {
        public static final TimeInterpolator easeIn = new TimeInterpolator() {
            public float getInterpolation (float input) {
                return DOMAIN*(input/=DURATION)*input + START;
            }
        };
        public static final TimeInterpolator easeOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return -DOMAIN *(input/=DURATION)*(input-2) + START;
            }
        };
        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return ((input/=DURATION/2) < 1) ?
                        (DOMAIN/2*input*input + START)
                            : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START);
            }
        };
    }

    static class Quart {
        public static final TimeInterpolator easeIn = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return DOMAIN*(input/=DURATION)*input*input*input + START;
            }
        };
        public static final TimeInterpolator easeOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START;
            }
        };
        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return ((input/=DURATION/2) < 1) ?
                        (DOMAIN/2*input*input*input*input + START)
                            : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START);
            }
        };
    }

    static class Quint {
        public static final TimeInterpolator easeIn = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return DOMAIN*(input/=DURATION)*input*input*input*input + START;
            }
        };
        public static final TimeInterpolator easeOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START;
            }
        };
        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return ((input/=DURATION/2) < 1) ?
                        (DOMAIN/2*input*input*input*input*input + START)
                            : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START);
            }
        };
    }

    static class Sine {
        public static final TimeInterpolator easeIn = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START;
            }
        };
        public static final TimeInterpolator easeOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START;
            }
        };
        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
            public float getInterpolation(float input) {
                return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START;
            }
        };
    }

}

 

あわせて読みたい

android1.6_2.png android1.6.png  

Android 3.0 以降で追加されたプロパティアニメーション機能の一部(ここ重要)をバックポートしたライブラリを作成しました。

 

このライブラリを使用すると、なんと! Android 1.6 環境でも、

独自に作成したオブジェクトのカスタムプロパティ(※)に限られますが、プロパティアニメーションをさせることができます。

(※Android 3.0 以降にしか存在しない View の組み込みプロパティ(x, y, alpha...など)は使用できない)

 

冒頭の画面キャプチャは、あの HT-03A(Android 1.6)上で Android 4.0 の API Demos に含まれる

プロパティアニメーションのサンプルが動作している様子です。

HT-03A(Android 1.6)たん可愛いよ。

IS01(Android 1.6)たん可愛いよ。

 

ソースコード

※Android 4.0 の API Demos のプロパティアニメーションのコードのうち、
 Android 1.6 + 本ライブラリで使用できる 6 つのサンプルコードも収録しています。

 

サポートしているクラス(制限事項あり)

  • Animator
  • AnimatorListenerAdapter
  • AnimatorSet
  • ArgbEvaluator
  • FloatEvaluator
  • FloatKeyframeSet
  • IntEvaluator
  • IntKeyframeSet
  • Keyframe
  • KeyframeSet
  • ObjectAnimator
  • PropertyValuesHolder
  • TimeAnimator
  • TypeEvaluator
  • ValueAnimator

※制限事項

変化させる対象のプロパティは独自に定義したものに限られます。
例えば、View の移動、回転、拡大縮小、アルファなどの組み込みプロパティは、
Android 3.0 以降のフレームワークに組み込まれているプロパティのため使用できません><

 

非サポートクラス

  • AnimatorInflater → リソースに定義したアニメーションからのインフレート
  • LayoutTransition → レイアウト変更時に適用するアニメーション
  • TimeInterpolator → 従来の android.view.Interpolator
  • ViewPropertyAnimator → View の組み込みプロパティに対する最適化されたアニメーション

 

バックポート手順

詳細なバックポート手順については下記リンク先(Google Drive(ログイン不要))に上げました。

https://docs.google.com/document/d/1fu9p3hfG8GDJOg6H6rIuWhhiGhsAQ4msMPb_pSiRI8g/edit

Google Code 内のリポジトリのコミットログにも細かくコメントをつけてますので、そちらを参照してもわかるかと思います。

コードを読むと、言語レベルでプロパティをサポートしていない Android において、

いかにしてプロパティを実現しているか理解できて面白いと思います。

一部、NDKを使用してネイティブコードも使用しています。

 

Androidプログラミング上達読本

あっ、そうそう、大事なこと言い忘れてました。

このたび、リックテレコムさんから出版される「Androidプログラミング上達読本」の Section 4 の執筆を担当しました。

同 Section にてプロパティアニメーションの API を解説していますので、あわせて読んでいただければ有難いです。

この本は、他にも、タオガクさんや、やんざむさんなど著名な方々の記事で構成されています。

2012年8月4日 ~10日 くらいには発売されますので、見かけた際には、是非手に取って確認してみてください。

book902.png

http://www.ric.co.jp/book/contents/book_902.html

 

私が書いた記事の構成としては、4-2  で API の解説をしていますが、

どちらかというと、4-2 は、困ったときに逆引き的な読めるように書いた/レイアウトしたので、

時間のない方は、プロパティアニメーションの魅力を理解するために

4-3、4-4 を斜め読みするのが良いかもしれません。

また、4-3 では、あの伝説の、、、「第4回 SHARPハッカソン」で生まれたスタ★ミやモグタソも登場します。

最後の 4-4 は、グラフ(カスタムビューのカスタムプロパティ)のアニメーションで、

Android 3.0 以降向けに作成したものですが、本エントリのバックポートライブラリを使用すると、

なんと Android 1.6 でも動作してしまいますー。

ちなみに、Google Drive に上げたバックポート手順は、紙面の都合上カットした没記事です^^;

30ページの枠ですが、勢い余って50ページ書いてました。。。

ということで、本エントリ以外にも、まだ隠し玉があったりします///

 

個人的には、プロパティアニメーションを使用したアプリが増えてくるのは・・・

2013年以降かな。。。

と思ってますので、末永く手元に置いていただければ・・・と思います^^;

 

あわせて読みたい

customLogo.png 

2012年7月8日、東京・京都・名古屋・中国・四国・九州・信州・米国 で同時開催された

DevFestX Japan 2012 Summer に参加してきました。

 

公式サイト

 

講演スライド(直リンク)

 

各会場からアップされた画像

最近 Google+ に追加された「イベント」機能のパーティーモードを使った写真

 

Togetter

@roishi2j2 さんによるまとめ

@tamacjp さんによるまとめ

 

お土産

「android ロボットのUSBメモリ」 や 「Google のビーチサンダル」 を頂きました。

usb.jpg

 

信州会場

Google Maps API Expert の 石丸(@kehi)さん、古籏(@openspc)さんの働きかけにより、

信州会場(パブリックビューイング)として開催していただけました。

「GTUG がない → DevFestX 行けない → しょぼーん(´・ω・`) 」 ではなく、

「ないなら創る → DevFestX やる → イヤッホゥ(゜∀゜)!!」 という流れです。

 

パブリックビューイングということでしたが、実際には他の会場と同様に Google+ Hangout で接続され、

地方にいながら、皆さんと同じ体験をすることができました。これも、

当日ぎりぎりまで準備してくださった Google Expert の方々、素晴らしい会場を提供してくださった S.I.P の方々、

裏方で運営をサポートしてくださった方々、満席になるほど参加して盛り上げてくださった皆様のおかげですね。

大変有難いことですm_ _m

 

@lychee さんのツイートにもありましたが、まさにそのとおりだと感じました。

221873380897460224.png

 

まとめると・・・

 

221834100988846080.png

 

九州会場凄い!ハングアウト凄い!

Android 4.1 (API Level 16)の ViewPropertyAnimator の変更点を調べました。

 

変更点

以下の3メソッドが追加されています。

  • withLayer()
  • withStartAction(Runnable)
  • withEndAction(Runnable)

 

解説

  • withLayer()

ViewPropertyAnimator を使用してアニメーションを実行する時、

アニメーション対象の View をハードウェアレイヤを使用してアニメーションさせます。

アニメーション終了後は、この設定はクリアされます。

ただし、ハードウェアアクセラレータが使用できない環境では、ソフトウェアレイヤが使用されます。

レイヤーの合成処理をハードウェアに任せることで高速化が期待できます。

 

  • withStartAction(Runnable) / withEndAction(Runnable)

ViewPropertyAnimator を使用したアニメーションの開始/終了時に実行したい処理を設定できます。

 

追加されたメソッドについて、実際に呼び出してみました。

以下のコードを実行すると、アニメーション開始時(3秒後)に、withStartAction() で指定した処理、

アニメーション終了時に、withEndAction() で指定した処理が呼び出されます。

また、withLayer() を呼び出しているので、ビュー(ここではボタン)のアニメーションは、ハードウェアレイヤ上で処理されます。

 

サンプルソースコード

final Button button = (Button) findViewById(R.id.button);

// 指定した View から ViewPropertyAnimator インスタンスを取得する
ViewPropertyAnimator vpa = button.animate();

// ハードウェアアクセラレータが使用できる環境では、ハードウェアレイヤを使用するように設定
vpa.withLayer();

// アニメーション継続期間を設定する(ミリ秒)
vpa.setDuration(3000);

// アニメーション開始時に実行する処理を設定する
vpa.withStartAction(new Runnable() {
    @Override
    public void run() {
        // アニメーション開始時に呼び出されます。
        // アニメーション開始遅延時間が設定されている場合は、
        // アニメーション開始遅延時間後に呼び出されます。
        String layerTypeString = "";
        int layerType = button.getLayerType();
        switch (layerType) {
            case View.LAYER_TYPE_NONE:
                // withLayer() を呼び出さなかった時
                layerTypeString = "LAYER_TYPE_NONE";
                break;
            case View.LAYER_TYPE_SOFTWARE:
                // withLayer() を呼出した時、
                // ハードウェアアクセラレータが使用できない環境では、ソフトウェアレイヤを使用される
                layerTypeString = "LAYER_TYPE_SOFTWARE";
                break;
            case View.LAYER_TYPE_HARDWARE:
                // withLayer() を呼出した時、
                // ハードウェアアクセラレータが使用できる環境では、ハードウェアレイヤを使用される
                layerTypeString = "LAYER_TYPE_HARDWARE";
                break;
                
        }
        Log.v("ViewPropertyAnimatorSample",
                "withStartAction() using " + layerTypeString);
    }
});

// アニメーション終了時に実行する処理を設定する
vpa.withEndAction(new Runnable() {
    @Override
    public void run() {
        // アニメーション修了時に呼び出されます。
        Log.v("ViewPropertyAnimatorSample", "withEndAction()");
        button.setVisibility(View.GONE);
    }
});

// ビューに対してアニメーションを設定する(-> アニメーションが開始されます)
vpa.rotation(90);

 

Android SDK Tools, Revision 20 から、システムワイドなトレースツールである systrace が追加されました。

このツールを使うと、ユーザランドとカーネルの詳細なトレース結果を得ることができるので、パフォーマンス解析に役立ちます。

 

準備

  • PC に「Android SDK Tools, Revision 20」をインストールする。
  • PC に Python (実行環境)をインストールする。
  • Android 4.1 端末を PC と接続する。
  • 端末上の設定アプリで「開発者オプション」 > 「トレースを有効にする」 で任意の有効なトレースを選択する。

 

systrace の使い方

android-sdk/tools/systrace ディレクトリに移動し、systrace.py を実行する(※)。

※実行するには「python systrace.py」、または、実行権(x)を付加して「./systrace.py」

デフォルトでは 5 秒間トレースされますが、-t でトレース時間(単位は秒)を指定できます。

systrace1.png

 

実行結果

実行すると、trace.html ファイルが生成されるので、Chrome ブラウザ(※)で開きます。

※Firefox では正常に動作しませんでした

 

下図は、「API Demos」 > 「Graphics」 > 「OpenGL ES」 > 「Sprite Text」 を 1 秒間トレースした結果です。

trace.html ファイルは、HTML + Javascript で操作できるようになっていて、

色のついているエリアをダブルクリックすると、拡大できたり、範囲選択すると、下側に詳細な説明が表示されます。

VSYNC 美しいです!

VSYNC 美しいです!

trace.png

 

おまけ

systrace の出力結果には、trace-viewer という Chrome の Javascript ベースなトレースツールを使っているようです。

trace-viewer is the javascript frontend for Chrome's about:tracing UI, and can be used to visualize and analyze traces from various tracing libraries, including the linux kernel and Chrome's base/trace_event system.

また、systrace ツール自体は、python スクリプトで記述されていて、内部的には、adb shell から  atrace を実行しているようです。

 

あわせて読みたい

Android 4.1(Jelly Bean) から Activiy 起動アニメーションが簡単に指定できるようになりました。

 

Activity 起動アニメーション種類

  • スケールアップ(ScaleUpAnimation)
  • サムネイルスケールアップ(ThumbnailScaleUpAnimation)
  • カスタムアニメーション(CustomAnimation)

 

使い方

  1. ActivityOptions クラスの静的メソッド(makeXXX)を呼び出して同クラスをインスタンス化する。
  2. インスタンス化した ActivityOptions を、startActivity の引数に渡す。

 

以下、それぞれのアニメーションの解説です。

 

スケールアップ(ScaleUpAnimation)

画面上の指定された開始位置から、新しいアクティビティを拡大するアニメーション。

 

  • ソースコード
// アニメーションの基点となるビュー
Button view = (Button) findViewById(R.id.button);

// スケールアップ用 ActivityOptions をインスタンス化
ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(
    view,
    0, 0, view.getWidth(), view.getHeight());

// アニメーションを指定してアクティビティを起動
startActivity(
    new Intent(this, SecondActivity.class),
    opts.toBundle());

 

サムネイルスケールアップ(ThumbnailScaleUpAnimation)

指定したサムネイル(Bitmap)を指定した位置から、新しいアクティビティを拡大するアニメーション。

 

  • ソースコード
// アニメーションの基点となるビュー
Button view = (Button) findViewById(R.id.button);

// ここではサンプルとしてスケールアップするアニメーションの画像に
// ボタンの画像(Bitmap)を使用していますが、任意の画像を使用することができます。
view.setDrawingCacheEnabled(true); // 後で元に戻すこと
view.setPressed(false);
view.refreshDrawableState();
Bitmap bitmap = view.getDrawingCache();

// サムネイルスケールアップ用 ActivityOptions をインスタンス化
ActivityOptions opts = ActivityOptions.makeThumbnailScaleUpAnimation(
    view,
    bitmap, 0, 0);

// アニメーションを指定してアクティビティを起動
startActivity(
    new Intent(this, SecondActivity.class),
    opts.toBundle());

view.setDrawingCacheEnabled(false);

 

カスタムアニメーション(CustomAnimation)

カスタムアニメーション(リソース)を指定して、新しいアクティビティを拡大するアニメーション。

 

  • ソースコード
// カスタムアニメーション用 ActivityOptions をインスタンス化
ActivityOptions opts = ActivityOptions.makeCustomAnimation(
    MainActivity.this,
    R.anim.zoom_enter, R.anim.zoom_exit);

// アニメーションを指定してアクティビティを起動
startActivity(
    new Intent(this, SecondActivity.class),
    opts.toBundle());

 

  • カスタムアニメーションリソース(サンプル)・・・R.anim.zoom_enter
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/decelerate_interpolator" >
    <scale
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXScale="2.0"
        android:fromYScale="2.0"
        android:pivotX="50%p"
        android:pivotY="50%p"
        android:toXScale="1.0"
        android:toYScale="1.0" />
</set>

 

  • カスタムアニメーションリソース(サンプル)・・・R.anim.zoom_exit
    • <set xmlns:android="http://schemas.android.com/apk/res/android"
          android:interpolator="@android:anim/decelerate_interpolator"
          android:zAdjustment="top" >
          <scale
              android:duration="@android:integer/config_mediumAnimTime"
              android:fromXScale="1.0"
              android:fromYScale="1.0"
              android:pivotX="50%p"
              android:pivotY="50%p"
              android:toXScale=".5"
              android:toYScale=".5" />
          <alpha
              android:duration="@android:integer/config_mediumAnimTime"
              android:fromAlpha="1.0"
              android:toAlpha="0" />
      </set>
      

       

      あわせて読みたい

      Android 4.1(Jelly Bean) から新しく 3 つのスタイルが使用できるようになりました。

      • BigTextStyle
      • BigPictureStyle
      • InboxStyle

      また、通知領域に簡単にボタンを追加できるようにもなっています。

       

      以下、それぞれの画面キャプチャとソースコードです。

      画面キャプチャと、ソースコード中の文字列を比較しながら見ると、わかりやすいと思います^^

       

      Notification.BigTextStyle

      BigTextStyle は「テキスト(Summary Text)」と「小さいアイコン」を表示できます。

       

      • 画面キャプチャ

      Notification.BigTextStyle.png

      •  ソースコード
      // NotificationManager
      NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      
      // Small and large icon
      int smallIconId = R.drawable.ic_action_search;
      Bitmap largeIconBitmap = BitmapFactory.decodeResource(
          getResources(), R.drawable.ic_launcher);
      
      //-----------------------------------------------------------------------
      // BigTextStyle
      //-----------------------------------------------------------------------
      Notification notificationBigTextStyle = new Notification.BigTextStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap))
          .bigText("BigText")
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle() を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(0, notificationBigTextStyle);
      

       

      Notification.BigPictureStyle

      BigPictureStyle は「大きな画像(Big Picture)」を表示できます。

       

      • 画面キャプチャ

      Notification.BigPictureStyle.png

      •  ソースコード
      //-----------------------------------------------------------------------
      // BigPictureStyle
      //-----------------------------------------------------------------------
      Notification notificationBigPictureStyle = new Notification.BigPictureStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap))
      //  .bigLargeIcon(largeIconBitmap) // Notification.Builder#setLargeIcon() を上書き
          .bigPicture(largeIconBitmap)
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle() を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(1, notificationBigPictureStyle);

       

      Notification.InboxStyle

      InboxStyle は、複数行の「テキスト(Line)」、「テキスト(Summary Text)」と「小さいアイコン」を表示できます。

      ※8 行目以降は、"..."表示されます

       

      • 画面キャプチャ

      Notification.InboxStyle.png

      •  ソースコード
      //-----------------------------------------------------------------------
      // InboxStyle
      //-----------------------------------------------------------------------
      Notification notificationInboxStyle = new Notification.InboxStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap))
          .addLine("addLine(1)")
          .addLine("addLine(2)")
          .addLine("addLine(3)")
          .addLine("addLine(4)")
          .addLine("addLine(5)")
          .addLine("addLine(6)")
          .addLine("addLine(7)")
          .addLine("addLine(8)") // 8行目以降は"..."で表示される
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle()  を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(2, notificationInboxStyle);

       

      ボタンを追加する

      Notification 作成時、Notification.Builder#addAction() を呼び出すと、ボタンを追加することができます。

      ※最大3つまで追加できます

      ※ボタン押下時のアクションは、PendingIntent で指定します

       

      • 画面キャプチャ

      NotificationWithButtons.png

       

      • ソースコード
      // PendingIntent
      PendingIntent pendingIntent = PendingIntent.getActivity(this,
          0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
      
      //-----------------------------------------------------------------------
      // BigTextStyle + Button
      //-----------------------------------------------------------------------
      Notification notificationBigTextStyle = new Notification.BigTextStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap)
              .addAction(android.R.drawable.ic_input_add, "Add", pendingIntent)
              .addAction(android.R.drawable.ic_input_delete, "Delete", pendingIntent))
          .bigText("BigText")
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle() を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(4, notificationBigTextStyle);
      
      1234567891011

      2013年4月

        1 2 3 4 5 6
      7 8 9 10 11 12 13
      14 15 16 17 18 19 20
      21 22 23 24 25 26 27
      28 29 30