@blog.justoneplanet.info

日々勉強

Android 4.0のグラフィックとアニメーション

原文はこちらです。結構適当に訳してますので間違いがあれば指摘していただけると助かります。

■ハードウェアアクセラレーションの有効化

android 4.0ではデフォルトでハードウェアアクセラレーションがONになった。4.0よりも低いレベルでは「android:hardwareAccelerated=”true”」をAndroidManifest.xmlの<application>タグに加えることでONにできる。

もっと2Dのレンダリングパイプラインを学びたいならオフィシャル開発ガイドを見るように。このガイドは様々なレベルでハードウェアアクセラレーションをコントロールする方法を説明し、様々なパフォーマンスチップスやトリックを提供、新しい描画モデルの詳細を説明している。

Google I/O 2011 の Androidハードウェアアクセラレーションを見るのがおすすめ。

■TextureViewについて

今日、OpenGLや動画コンテンツを必要とするアプリケーションはSurfaceViewを呼び出した特別なUI要素に頼っている。このウィジェットはアプリケーションのウィンドウの後方に配置された新しいウィンドウを作ることによって動作する。新しいウィンドウを見せるためにアプリケーションのウィンドウ全体に穴を開ける。このアプローチは(アプリケーションウィンドウの再描画なしに新しいウィンドウのコンテンツがリフレッシュされる)とても効率的な一方、幾つかの重大な制限に苦しむ。

SurfaceViewのコンテンツはアプリケーションのウィンドウでは生きていないので効率的に(移動・リサイズ・回転)変形できない。これはListViewやScrollViewの内部でSurfaceViewを使うことを難しくさせる。SurfaceViewも退色した輪郭やView.setAlpha()(で半透明になったView)のようなUI toolkitと上手く相互作用できない。

これらの問題を解決するために、android 4.0はハードウェアアクセラレーションされた2DレンダリングパイプラインとSurfaceTextureによるTextureViewと呼ばれる新しいウィジェットを導入した。TextureViewはSurfaceViewと同じ能力を提供するが通常のViewと同じように振る舞う。例えばOpenGLを表示したり動画ストリームを表示したりできる。

下述のコードはカメラから動画のプレビューを表示するためにTextureViewを作っている。TextureViewは45度回転し半梅井になっている。(コードはそのまま引用)

public class TextureViewActivity extends Activity implements TextureView.SurfaceTextureListener {
    private Camera mCamera;
    private TextureView mTextureView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mTextureView = new TextureView(this);
        mTextureView.setSurfaceTextureListener(this);

        setContentView(mTextureView);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        mCamera = Camera.open();

        Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
        mTextureView.setLayoutParams(new FrameLayout.LayoutParams(
                previewSize.width, previewSize.height, Gravity.CENTER));

        try {
            mCamera.setPreviewTexture(surface);
        } catch (IOException t) {
        }

        mCamera.startPreview();
        
        mTextureView.setAlpha(0.5f);
        mTextureView.setRotation(45.0f);
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // Ignored, the Camera does all the work for us
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        mCamera.stopPreview();
        mCamera.release();
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // Called whenever a new frame is available and displayed in the TextureView
    }
}

アニメーション

まず、もしも3.0と3.1で加えられたandroid.animationパッケージとクラスを見たことがないならば、Animation in HoneycombIntroducing ViewPropertyAnimatorを読むといいかもしれない。 これらの記事はandroidでアニメーションを簡単にパワフルでフレキシブルにする3.0で追加されたAPIを説明する。下述で説明されているAndroid 4.0の進化はこれらのコアの機能に小さな追加をしたものである。

CakePHP 2.0でログインフォームを作る

■実装

app/Controller/UsersController.php

App::uses('AppController', 'Controller');
/**
 * Users Controller
 * @property User $User
 */
class UsersController extends AppController {

    public $uses = array('User');
    
    public $components = array(
        'Session',
        'Auth' => array(
            'loginRedirect' => array('controller'  => 'users', 'action' => 'index'),// ログイン後のリダイレクト先
            'logoutRedirect' => array('controller' => 'pages', 'action' => 'display', 'home')// ログアウト後のリダイレクト先
        )
    );
    
    public function beforeFilter() {
        //$this->Auth->allow('index'/*, 'add'*/);// 認証を除外するアクションを指定する
    }

    /**
     * ユーザを追加する画面
     * @return void
     */
    public function add() {
        /*if ($this->request->is('post')) {
            $this->User->create();
            if ($this->User->save($this->request->data)) {
                $this->Session->setFlash(__('The user has been saved'));
                $this->redirect(array('action' => 'index'));
            }
            else {
                $this->Session->setFlash(__('The user could not be saved. Please, try again.'));
            }
        }*/
    }

    /**
     * ログイン画面
     * @return void
     */
    public function admin_login() {
        if ($this->Auth->login()) {
            $this->redirect($this->Auth->redirect());
        }
        else {
            $this->Session->setFlash(__('Invalid username or password, try again'));
        }
    }

    /**
     * ログアウト画面
     * @return void
     */
    public function admin_logout() {
        $this->redirect($this->Auth->logout());
    }
}

app/Model/User.php

App::uses('AppModel', 'Model');

/**
 * User Model
 * ユーザのモデル
 */
class User extends AppModel {
    public function __construct($id = false, $table = null, $ds = null) {
        $this->useDbConfig = Configure::read('Config.environment');
        parent::__construct($id, $table, $ds);
    }

    /**
     * 保存前に実行される
     * @return void
     */
    public function beforeSave() {
        if (isset($this->data[$this->alias]['password'])) {
            $this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password']);
        }
        return true;
    }

}

Cake1系ではpasswordというカラム名が自動的にハッシュ化されていた。Cake2系では上述のように実装する必要がある。

app/View/Users/admin_login.ctp

以下のようにログインフォームを記述する。

<?php echo $this->Session->flash('auth'); ?>
<?php echo $this->Form->create('User');?>
<fieldset>
<legend><?php echo __('Please enter your username and password'); ?></legend>
<?php
echo $this->Form->input('username');
echo $this->Form->input('password');
?>
</fieldset>
<?php echo $this->Form->end(__('Login'));?>

$thisを介してSessionとFormにアクセスするところがCake2系では異なる。

app/View/Users/admin_logout.ctp

<h3>ログアウトしました。</h3>

■参考

殆ど参考文献の簡略化日本語版だな。

androidのSQLiteでAUTOINCREMENT値を取得する

SELECT `seq` FROM sqlite_sequence WHERE `name` = 'TABLENAME';

上述を踏まえて以下のようにすることで取得できる。

    /**
     * auto_incrementで代入した最大値
     * @return
     */
    public long getLastInsertId() {
        long index = 0;
        SQLiteDatabase sdb = getReadableDatabase();
        Cursor cursor = sdb.query(
                "sqlite_sequence",
                new String[]{"seq"},
                "name = ?",
                new String[]{TABLENAME},
                null,
                null,
                null,
                null
        );
        if (cursor.moveToFirst()) {
            index = cursor.getLong(cursor.getColumnIndex("seq"));
        }
        cursor.close();
        return index;
    }

robotiumを使ってみる

@ayunyanさんに教えていただいたので使ってみる。

■シナリオ

以下のようにandroid版顔文字辞典を操作するとする。

  1. メニューを開く
  2. カテゴリ追加をタップする
  3. カテゴリ名を入力し登録する(a)
  4. 登録したカテゴリ(a)をロングタップする
  5. 編集を選択する
  6. カテゴリ名を変更して登録する(b)
  7. 編集したカテゴリ(b)をロングタップする
  8. 削除をタップする

■テストコード

以下のようにして「みんなの顔文字辞典」のカテゴリー追加・編集・削除のダイアログのテストを行う。

package info.justoneplanet.android.kaomoji.test.dialog;

import com.jayway.android.robotium.solo.Solo;

import info.justoneplanet.android.kaomoji.KaomojiFavorite;
import info.justoneplanet.android.kaomoji.R;
//import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2;

/**
 * カテゴリを追加するダイアログのテストケース
 * @author justoneplanet
 * カテゴリの登録・編集・削除は決まった順番で行われる必要があるためメソッド名で順序を指定した
 */
public class CategoryDialogBuilderTest extends ActivityInstrumentationTestCase2<KaomojiFavorite> {

    private KaomojiFavorite mActivity;
    //private Instrumentation mInstrumentation;
    private Solo solo;
    
    private static final String OLD_CATEGORY_NAME = "新しいカテゴリ";
    private static final String NEW_CATEGORY_NAME = "新カテゴリ";

    public CategoryDialogBuilderTest() {
        super("info.justoneplanet.android.kaomojifavorite", KaomojiFavorite.class);
    }
    
    @Override
    public void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        //mInstrumentation = getInstrumentation();
        solo = new Solo(getInstrumentation(), getActivity());
    }
    
    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        mActivity = null;
        //mInstrumentation = null;
    }
    
    /**
     * カテゴリを正しく追加できるか
     * カテゴリaを登録する
     */
    public void testCategory1Add() {
        solo.clickOnMenuItem(mActivity.getString(R.string.menu_category_add));// メニュー>カテゴリを追加
        solo.enterText(0, OLD_CATEGORY_NAME);// 新しいカテゴリ名を入力
        solo.clickOnButton(mActivity.getString(R.string.register));// 登録ボタン
    }
    
    /**
     * カテゴリを正しく編集できるか
     * カテゴリaを編集してカテゴリbに名前を変える
     */
    public void testCategory2Edit() {
        solo.clickLongOnText(OLD_CATEGORY_NAME);// テキストで探索してリストの要素をロングタップ
        solo.clickOnText(mActivity.getString(R.string.edit));// ダイアログ内カテゴリの編集をタップ
        solo.clearEditText(0);// テキストを消す
        solo.enterText(0, NEW_CATEGORY_NAME);// 新カテゴリ名を入力
        solo.clickOnButton(mActivity.getString(R.string.register));// 登録ボタン
    }
    
    /**
     * カテゴリを削除できるか
     * カテゴリbを削除する
     */
    public void testCategory3Delete() {
        solo.clickLongOnText(NEW_CATEGORY_NAME);// テキストで探索してリストの要素をロングタップ
        solo.clickOnText(mActivity.getString(R.string.delete));// カテゴリの削除をクリック
        solo.waitForDialogToClose(1000);// すぐに終了してしまうと削除クエリが実行されなかったので
    }
}

操作する要素をテキストで指定できる探索が便利!

clickInListについて

以下のようにして使用した。おそらく表示しているlistアイテムの配列の中でのpositionを指定していると思われる。また、表示されていないlistアイテムを上からの位置で指定することはできなかった。

private String tapEmoticonAfterCategory(String category, int position) {
    solo.clickOnText(category);
    solo.waitForDialogToClose(1000);// 作用するまでwait
    ArrayList<TextView> list = solo.clickInList(position);// 表示されているlistの要素配列のposition
    solo.waitForDialogToClose(1000);// 作用するまでwait
    Log.e("tapped", String.valueOf(list.get(0).getText()));
    return String.valueOf(list.get(0).getText());
}

■参考

おまけ

当初は以下のようにThread.sleepを使用していたが、ProgressDialogを表示し通信結果を待つような場合に期待した動作にならなかった。

    /**
     * カテゴリを削除できるかテストする
     * @throws InterruptedException 
     */
    public void testCategory3Delete() throws InterruptedException {
        solo.clickLongOnText(NEW_CATEGORY_NAME);// テキストで探索してリストの要素をロングタップ
        solo.clickOnText(mActivity.getString(R.string.delete));// カテゴリの削除をクリック
        Thread.sleep(1000);// 期待の動作とならない
    }

テキスト(半角カナ?)によってはいまいち探索できないときがある。

AsyncTaskのテストをする

■コード

以下のようにUIスレッドでAsyncTaskをインスタンス化する必要がある。

public class EveryoneTaskTest extends ActivityInstrumentationTestCase2<Kaomoji> {
    private Kaomoji mActivity;
    private Instrumentation mInstrumentation;
    private EveryoneTask everyoneHttp;
    private CountDownLatch countDownLatch;
    private String mResult;
    
    public EveryoneTaskTest() {
        super("info.justoneplanet.android.kaomoji", Kaomoji.class);
    }
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        mInstrumentation = getInstrumentation();
        countDownLatch = new CountDownLatch(1);
    }
    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        mActivity = null;
        mInstrumentation = null;
    }
    
    public void testExecute() throws InterruptedException, JSONException {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                everyoneHttp = new EveryoneTask(new EveryoneTask.Observer(){
                    @Override
                    public void onHttpUpdate(String result, MODE mode) {
                        mResult = result;
                        countDownLatch.countDown();// カウントを0にする
                    }
                });
                everyoneHttp.execute(null);
            }
        });
        countDownLatch.await();// countが0になるまで処理を待機
        
        // 要素の検証
        JSONObject elm;
        JSONArray ary = new JSONArray(mResult);
        assertTrue(ary.length() == 150);
    }
}

失敗例

以下のようにアプリのメインスレッド以外でAsyncTaskのインスタンス化を行なってはならない。挙動が不安定になりonPostExecuteが実行されない。

/**
 * みんなな顔文字を通信して取得するクラスのテストケース
 * @author justoneplanet
 */
public class EveryoneHttpTest extends AndroidTestCase implements EveryoneHttp.Observer {
    CountDownLatch countDownLatch;
    private EveryoneHttp everyoneHttp;
    private String mResult;
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        countDownLatch = new CountDownLatch(1);
    }

    public void testDoInBackground() throws InterruptedException, JSONException {
        everyoneHttp = new EveryoneHttp(this);// get everyone's emoticons from transfer
        everyoneHttp.execute();
        countDownLatch.await();// countが0になるまで処理を待機
        
        JSONArray ary = new JSONArray(mResult);
        assertTrue(ary.length() == 150);// 要素数の検証
        
        // 要素の検証
        JSONObject elm = ary.getJSONObject(0);
        Log.e("elm", elm.toString());
        assertFalse(elm.getString("face").equals(null));
        assertFalse(elm.getString("tag").equals(null));
    }
    
    /**
     * 通信完了後に実行される
     */
    @Override
    public void onHttpUpdate(String result) {
        mResult = result;
        countDownLatch.countDown();// カウントを0にする
    }
}

非同期処理自体はAsyncTaskをwrapしたクラスで行なっている。Observerを使ったのでテストコードもすっきり。

androidでAndroidWebDriverを使ってみる

元ネタはTim BrayのIntroducing Android WebDriverという記事。

■準備

テストプロジェクトで右クリックし「propaties> Java Build Path > Libraries > Add External JARs」で以下のjarをインポートする。

  • android_webdriver_library-srcs.jar
  • android_webdriver_library.jar
  • guava-10.0.1.jar

■コード

Tim様の記事ではgoogle.comが使用されているが以下のような理由によってテストにパスしない。

  • ロケールが違うので日本語表示される
  • 結果がリッチすぎて上手くdomを抽出できない

テストコード内でgoogle.comのリンクをクリックするようにできると思うが、html構造を把握するのが大変なので以下のようなコードでmozillaのサイトでテストすることにした。

package bootcamp2011.android.test;

import java.util.List;

import bootcamp2011.android.Bootcamp2011Activity;
import android.test.ActivityInstrumentationTestCase2;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.android.AndroidWebDriver;;

public class Bootcamp2011ActivityTest extends
        ActivityInstrumentationTestCase2<Bootcamp2011Activity> {

    private AndroidWebDriver driver;

    public Bootcamp2011ActivityTest() {
        super("bootcamp2011.android.bootcamp2011activity", Bootcamp2011Activity.class);
    }
    
    @Override
    protected void setUp() throws Exception {
      driver = new AndroidWebDriver(getActivity());
    }
    
    @Override
    protected void tearDown() {
       driver.quit();
    }
    
    public void testGoogleShouldWork() {
        // mozillaのサイトを訪問
        driver.get("https://developer.mozilla.org/ja/JavaScript");

        // 検索boxを抽出して文字入力し送信
        WebElement searchBox = driver.findElement(By.id("q"));
        searchBox.sendKeys("querySelector");
        searchBox.submit();

        // 検索結果にquerySelectorという文字列が含まれているかテスト(仮)
        WebElement resultSection = driver.findElement(By.id("search-results"));
        List<WebElement> searchResults = resultSection.findElements(By.tagName("table"));
        WebElement result = searchResults.get(0);
        assertTrue(result.getText().contains("querySelector"));
    }
}

By.cssSelector()というメソッドがあるのだが上手く働かない…一応、動作するサンプルが「android-sdk-mac_x86/extras/google/webdriver/TestAnAndroidWebApp/src/simple/app/test/SimpleGoogleTest.java」にある。