2013年8月14日水曜日

[node.js] Windows7 + Node.js + Express + SQLite3

Windows7 に Node.js + Express + SQLite3 環境を作ったのですが、SQLite3 のインストールでつまづいたのでメモ。

【前準備 その1】 Python インストール (※SQLite インストールに必要)
http://www.python.jp/download/ から python-2.7.5.amd64.msi を取得し、「C:\Python27」にインストール後、パスを通す

【前準備 その2】 「vcvarsall.bat」をインストール (※SQLite インストールに必要)
当該バッチは VS2010 と互換性のあるバージョンの Windows SDK についてくるようです。僕は VisualStudio 2010 をインストールしていたので下記フォルダにありました。
「C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\vcvarsall.bat」

【前準備 追記】
「vcvarsall.bat」を使ったのは、
$ npm install sqlite3
を OS 付属のコマンドプロンプトを使って実行すると↓みたいなエラーが出てうまく行かなかったんだけれども「vcvarsall.bat」を使ったら解決したよ。っていう人がいたから。でも、複数回コマンドプロンプトから試してみたらうまく行った。「vcvarsall.bat」は特に必要ないのかしらん... (汗) Python はないとエラーになるはず (`・ω・´)
...
C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\Microsoft.CppCommon.targets(5
74,5): error MSB6006: "mt.exe" はコード 31 を伴って終了しました。 [C:\Users\higu
chi_yuki\Dropb
ox\node.js\testapp\node_modules\sqlite3\build\node_sqlite3.vcxproj]
gyp ERR! build error
gyp ERR! stack Error: `c:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe
` failed with exit code: 1
...

1. Node.js をインストール
http://nodejs.org/download/ から node-v0.10.15-x64.msi を取得して実行

2. Express をグローバル環境に (-g オプション追加) インストール
$ npm install -g express

3. アプリケーションフォルダ「sample」を作成し、初期化
(※アプリケーションフォルダの親フォルダで実行のこと)
$ mkdir sample
$ express -e sample
$ cd sample && npm install

4. 「Visual Studio コマンド プロンプト *1」を利用して SQLite3 インストール
$ npm install sqlite3
*1 ... 「Visual Studio コマンド プロンプト」は、僕が VisualStudio 2010 をインストールしたことによる表現です。実体は「前準備 その2」の「vcvarsall.bat」です。

起動方法は、[スタート] - [すべてのプログラム] - [Microsoft Visual Studio 2010] - [Visual Studio Tools] - [Visual Studio コマンド プロンプト (2010)]です。

成功するとこんなかんじになります。


5. SQLite のテーブルから読みだした内容を表示するサンプルをば

SQLite のファイルを準備します。ここでは PupSQLite を使って sample.db を作りました。

フォルダの様子はこんなかんじ。


sample/app.jp
var express = require('express');
var routes = require('./routes');
var http = require('http');
var path = require('path');

var app = express();

app.set('port', process.env.PORT || 1234);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

app.get('/', routes.index);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});
sample/routes/index.js
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('sample.db');

exports.index = function(req, res){
  db.serialize(function(){
    db.all("select * from messages", function(err, rows){
      if (!err) {
        res.render('index', {
          title: 'Node.js + Express + SQLite3',
          list: rows,
        });
      }
    });
  });
};
sample/views/index.ejs
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <ul>
    <% list.forEach(function(message){ %>
      <li><%= message.content %></li>
    <% }) %>
    </ul>
  </body>
</html>

実行結果はこんなかんじ。


6. 参考にさせてもらったサイト

なんてステキなんだろう Node.js ... Raspberry Pi にも入れてみたいです!

2013年3月15日金曜日

[Android TIPS] Activity アニメーションの終了を検知する

ということをやりたかったのですが、特に調べることもせず、てっとり早く「アニメーションの所要時間 x 2 が経過したら終わってるでしょ」という方針で。なので onStart() でスレッド立てて、sleep して、その後に検知後の操作をする事にしました。
ただし、アニメーションの所要時間と sleep で待つ時間はリソースの同じ個所を参照するように気をつけました。

参考にさせてもらったソース等々は 「throw Life - ActivityのOpenとCloseをアニメーションさせる」 です。以下のソースは差分があるところをちらほら。


res/values/anims.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <integer name="animation_duration">1000</integer>
</resources>

res/anim/activity_open_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
  android:interpolator="@android:anim/accelerate_interpolator" >
  <translate
    android:duration="@integer/animation_duration"
    android:fillAfter="true"
    android:fillEnabled="true"
    android:fromYDelta="100%"
    android:toYDelta="0%" />
</set>

Activity1 .java
package com.sample.activityanimation;

import android.os.Bundle;
import android.os.SystemClock;
import android.app.Activity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

public class Activity1 extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity1);

    Button btnBack = (Button) findViewById(R.id.btnBack);
    btnBack.setOnClickListener(new OnClickListener() {
      public void onClick(View v) {
        finish();
      }
    });
  }

  @Override
  protected void onStart() {
    super.onStart();

    new Thread(new Runnable() {
      @Override
      public void run() {
        int duration = Activity1.this.getResources().getInteger(R.integer.animation_duration);
        SystemClock.sleep(duration * 2);

        Activity1.this.runOnUiThread(new Runnable() {
          @Override
          public void run() {
            Toast.makeText(Activity1.this, "Hello world!!", Toast.LENGTH_SHORT).show();
          }
        });
      }
    }).start();
  }
}

もっとスマートな方法を知りたい...

[Android TIPS] Bitmap.createBitmap ではなく Canvas を使って画像を回転する

SD カードに配置した画像 (img_src.jpg) をサイズ圧縮し、回転なし画像 (img_dst1.jpg) と
回転あり画像 (img_dst2.jpg) として保存するアプリを作ってみました。
Matrix + Bitmap.createBitmap で回転しようとすると Out of Memory で落ちてしまうので、
回転した Canvas に画像を描画することで実現してみました。

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dip" >

    <ImageView
        android:id="@+id/imgPreview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sample.bitmaprotate"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="17" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.sample.bitmaprotate.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
MainActivity.java
package com.sample.bitmaprotate;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

import android.os.Bundle;
import android.os.Environment;
import android.widget.ImageView;
import android.widget.Toast;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // プレビュー用ウィジェット
    ImageView imgPreview = (ImageView) findViewById(R.id.imgPreview);

    // SD カード内の対象画像の読み込み (img_src.jpg)
    String path = Environment.getExternalStorageDirectory() + "/img_src.jpg";
    if (!new File(path).exists()) {
      Toast.makeText(this, "エラー: SDカードに img_src.jp を格納して再度実行してください。", Toast.LENGTH_LONG).show();
      return;
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 2; // 1/4のサイズで読み込み
    options.inPreferredConfig = Bitmap.Config.RGB_565; // 低クオリティでの読み込み
    Bitmap bmpSrc = BitmapFactory.decodeFile(path, options);
    // [圧縮済 回転'なし'] 画像を SD カード内に保存 (img_dst1.jpg)
    save(bmpSrc, "img_dst1.jpg");

    // 90度回転したキャンバスを用意
    Bitmap bmpDst = Bitmap.createBitmap(bmpSrc.getHeight(), bmpSrc.getWidth(), Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(bmpDst);
    canvas.save();
    canvas.rotate(90, bmpSrc.getWidth() / 2, bmpSrc.getHeight() / 2);

    // 回転後の中心位置のずれを考慮して画像を描画
    float diff = bmpSrc.getWidth() / 2 - bmpSrc.getHeight() / 2;
    canvas.drawBitmap(bmpSrc, diff, diff, null);

    // 回転をもとに戻す → 結果的に描画した画像が回転することとなる
    canvas.restore();

    // プレビュー
    imgPreview.setImageBitmap(bmpDst);

    // [圧縮済 回転'あり'] 画像を SD カード内に保存 (img_dst2.jpg)
    save(bmpDst, "img_dst2.jpg");
  }

  /**
   * 画像の保存
   * 
   * @param bmp 対象画像
   * @param name ファイル名
   */
  private void save(Bitmap bmp, String name) {
    OutputStream out = null;
    String path = Environment.getExternalStorageDirectory() + "/" + name;

    try {
      File file = new File(path);
      if (file.createNewFile()) {
        out = new FileOutputStream(file);
        bmp.compress(CompressFormat.JPEG, 70, out);
      }

    } catch (Exception ex) {
      ex.printStackTrace();

    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (Exception ex2) {}
      }
    }
  }
}
本当はオリジナル画像の解像度を落とさず無圧縮で回転したかったのですがその方法は分からず...
NDK を使わないといけなかったりするもんでしょうか?

2013年1月14日月曜日

[Android TIPS] 色々な情報をリストに表示する

色々な情報をリストに表示するというのをやってみました。
表示対象は画像と文字列ですが、予め res/drawable とコード内に用意しました
国旗の画像は ここ から取得し、flag01.png ~ flag08.png とし、res/drawable 配下に配置しています。

サンプル野良apkはこちらからどうぞ。

レイアウトはこんな感じ。

activity_country.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <HorizontalScrollView
        android:id="@+id/scrollView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="#FFFFFF" >

        <LinearLayout
            android:id="@+id/listCountry"
            android:layout_width="wrap_content"
            android:layout_height="fill_parent"
            android:paddingLeft="4dip"
            android:paddingRight="4dip" />
    </HorizontalScrollView>

</FrameLayout>

info_country.xml

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >

    <TableRow>
        <ImageView
            android:id="@+id/imgFlag"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_span="2"
            android:scaleType="center" />
    </TableRow>

    <TableRow>
        <TextView
            android:layout_width="38dip"
            android:layout_height="wrap_content"
            android:text="国名:" />

        <TextView
            android:id="@+id/txtTitle"
            android:layout_width="142dip"
            android:layout_height="wrap_content" />
    </TableRow>

    <TableRow>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="首都:" />

        <TextView
            android:id="@+id/txtCapital"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </TableRow>

    <TableRow>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="通貨:" />

        <TextView
            android:id="@+id/txtCurrency"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </TableRow>

</TableLayout>

コードはこんな感じ。

package com.sample.sampleapp;

import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.app.Activity;

public class CountryActivity extends Activity {

 /** WRAP_CONTENT */
 private static final int WC = LinearLayout.LayoutParams.WRAP_CONTENT;

 /**
  * 国情報保持クラス
  */
 private static class Country {
  /** 国旗画像のリソース ID */
  public int flag;
  /** 国名 */
  public String title;
  /** 首都 */
  public String capital;
  /** 通貨 */
  public String currency;

  public Country(int flag, String title, String capital, String currency) {
   this.flag = flag;
   this.title = title;
   this.capital = capital;
   this.currency = currency;
  }
 }

 /** 国情報リスト */
 private static final Country[] mCountries = {
   new Country(R.drawable.flag01, "バハマ", "ナッソー", "バハマ・ドル"),
   new Country(R.drawable.flag02, "バングラディシュ", "ダッカ", "タカ"),
   new Country(R.drawable.flag03, "ベナン", "ポルトノボ", "CFAフラン"),
   new Country(R.drawable.flag04, "カメルーン", "ヤウンデ", "CFAフラン"),
   new Country(R.drawable.flag05, "コロンビア", "ボゴタ", "コロンビア・ペソ"),
   new Country(R.drawable.flag06, "デンマーク", "コペンハーゲン", "デンマーク・クローネ"),
   new Country(R.drawable.flag07, "エクアドル", "キト", "アメリカ合衆国ドル"),
   new Country(R.drawable.flag08, "エルサルバドル", "サンサルバドル", "アメリカ合衆国ドル"),
 };

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_country);

  // ウィジェットの初期化
  initWidget();
 }

 /**
  * ウィジェットの初期化
  */
 private void initWidget() {
  int len = mCountries.length;
  for (int i = 0; i < len; i++) {
   addCountry(i);
  }
 }

 /**
  * 国情報の追加
  * 
  * @param index
  */
 private void addCountry(int index) {
  // レイアウトをインフレート
  View v = this.getLayoutInflater().inflate(R.layout.info_country, null);

  // 国旗
  ImageView imgFlag = (ImageView) v.findViewById(R.id.imgFlag);
  imgFlag.setImageResource(mCountries[index].flag);

  // 国名
  TextView txtTitle = (TextView) v.findViewById(R.id.txtTitle);
  txtTitle.setText(mCountries[index].title);

  // 首都
  TextView txtCapital = (TextView) v.findViewById(R.id.txtCapital);
  txtCapital.setText(mCountries[index].capital);

  // 通貨
  TextView txtCurrency = (TextView) v.findViewById(R.id.txtCurrency);
  txtCurrency.setText(mCountries[index].currency);

  // レイアウトパラメータの調整
  LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(WC, WC);
  params.setMargins(4, 8, 4, 8);
  v.setLayoutParams(params);

  // リストに追加
  LinearLayout listCountry = (LinearLayout) findViewById(R.id.listCountry);
  listCountry.addView(v);
 }
}

TableLayout で幅を設定するのに、任意業の要素1つ1つに幅を設定しなければいけないものなのかしらん。

[Android TIPS] キーワード履歴に使えそうなボタン配置

キーワード履歴に使えそうなボタン配置を作ってみました。
こういう配置を自動的にやってくれるレイアウトとかはないですよね。たぶん(汗)





サンプル野良apkはこちらからどうぞ。

レイアウトはこんな感じ。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <EditText
            android:id="@+id/editInput"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/btnAdd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Add" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/layoutTag1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:id="@+id/layoutTag2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:id="@+id/layoutTag3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

コードはこんな感じ。

package com.sample.sampleapp;

import java.util.ArrayList;

import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.app.Activity;
import android.graphics.Paint;

public class TagActivity extends Activity {

 /** WRAP_CONTENT */
 private final int WC = ViewGroup.LayoutParams.WRAP_CONTENT;

 /** タグのラベル最大長 */
 private final int MAX_LABEL_LEN = 8;

 /** ボタンのパディング */
 private final int BTN_PADDING = 8;

 /** 横幅に占めるボタンに必要な最低幅の割合 */
 private final float RATIO_BUTTONS = 0.8f;

 /** タグを表示するリニアレイアウト */
 private final int[] mListTagLayout = { R.id.layoutTag1, R.id.layoutTag2, R.id.layoutTag3 };

 /** タグボタンのリスト */
 private final ArrayList<Button> mListTagButton = new ArrayList<Button>();

 /** 画面幅 */
 private int mScreenWidth = 0;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_tag);

  // 画面幅の取得
  WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
  mScreenWidth = wm.getDefaultDisplay().getWidth();

  // ウィジェットの初期化
  initWidget();
 }

 /**
  * ウィジェットの初期化
  */
 private void initWidget() {
  // 「add」ボタンを非活性化
  final Button btnAdd = (Button) findViewById(R.id.btnAdd);
  btnAdd.setEnabled(false);

  // 文字が入力されている場合に「add」ボタンを活性化
  final EditText editInput = (EditText) findViewById(R.id.editInput);
  editInput.addTextChangedListener(new TextWatcher() {
   @Override
   public void onTextChanged(CharSequence s, int start, int count, int after) {
   }

   @Override
   public void beforeTextChanged(CharSequence s, int start, int before, int count) {
   }

   @Override
   public void afterTextChanged(Editable s) {
    btnAdd.setEnabled(0 < s.length());
   }
  });

  // 「add」ボタン押下時に入力された文字列を元にタグを生成
  btnAdd.setOnClickListener(new OnClickListener() {
   @Override
   public void onClick(View v) {
    // ボタンを生成
    final Button btn = new Button(TagActivity.this);
    btn.setText(getInputText());
    btn.setPadding(BTN_PADDING, BTN_PADDING, BTN_PADDING, BTN_PADDING);
    btn.setOnClickListener(new OnClickListener() {
     @Override
     public void onClick(View v) {
      editInput.setText(btn.getText());
     }
    });
    // 同一の内容があれば削除
    for (int i = mListTagButton.size() - 1; 0 <= i; i--) {
     if (mListTagButton.get(i).getText().equals(btn.getText())) {
      mListTagButton.remove(i);
     }
    }
    mListTagButton.add(btn);

    // ボタンを配置
    int index = mListTagButton.size() - 1;
    for (int id : mListTagLayout) {
     LinearLayout targetLayout = (LinearLayout) findViewById(id);
     targetLayout.removeAllViews();

     // リストの末尾 (直近に生成されたボタン) から配置
     while (0 <= index) {
      Button target = mListTagButton.get(index);

      // 同一行のボタンに必要なボタン幅を算出
      int totalWidth = 0;
      for (int i = 0; i < targetLayout.getChildCount(); i++) {
       Button child = (Button) targetLayout.getChildAt(i);
       totalWidth += getVirtualButtonWidth(child);
      }
      // 同一行のボタン幅と次に追加するボタンの幅が規定値より大きければ次の行へ
      if (mScreenWidth * RATIO_BUTTONS < totalWidth + getVirtualButtonWidth(target)) {
       break;
      }

      // 対象行へボタンを配置
      LayoutParams params = new LinearLayout.LayoutParams(0, WC);
      params.weight = getVirtualButtonWidth(target);
      targetLayout.addView(target, params);

      index--;
     }
    }

    editInput.setText("");
   }
  });
 }

 /**
  * 入力文字列の取得 (規定文字数以上は省略)
  * 
  * @return
  */
 private String getInputText() {
  final EditText editInput = (EditText) findViewById(R.id.editInput);
  String input = editInput.getText().toString();

  if (MAX_LABEL_LEN < input.length()) {
   input = input.substring(0, MAX_LABEL_LEN) + "...";
  }

  return input;
 }

 /**
  * ボタンの仮想幅を取得
  * 
  * @param button
  * @return
  */
 private int getVirtualButtonWidth(Button button) {
  Paint p = new Paint();
  p.setTextSize(button.getTextSize());
  int labelWidth = (int) p.measureText(button.getText().toString());
  return (labelWidth + button.getTotalPaddingLeft() + button.getTotalPaddingRight());
 }
}

「ボタンの仮想幅」はそのボタンを表示した時にラベルがきちんと見える最小の幅を算出したかったので書いてみましたが、これでよいかしらん。でも「i」とか表示した際に潰れることはないから、これでもよいかしらん。

2012年6月21日木曜日

[Android TIPS] iPhone みたいな ToggleButton

iPhone みたくスイッチ時にアニメーションする ToggleButton を自作してみました。
トグルボタンをタップすると8段階にアニメーションして状態が切り替わります。


サンプルの野良 apk はこちらからどうぞ。


パッケージ・エクスプローラーはこんなかんじ。


追加したのは下記のファイル
  • CustomToggleButton.java
  • toggle_off.xml
  • toggle_on.xml
  • toggle01~09.png

toggle01~09.png のボタンはこんなかんじ。


では、コードを順番に。まずは CustomToggleButton.java から。
package com.sample.togglesample;

import android.content.Context;
import android.graphics.drawable.AnimationDrawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;

/**
 * カスタムトグルボタンクラス
 */
public class CustomToggleButton extends ImageView {

 /** 値変更イベントリスナ */
 private OnToggleChangeListener mListener;

 /** チェック状態 */
 private boolean mChecked;

 /** チェック状態: setter */
 public boolean isChecked() {
  return mChecked;
 }

 /** チェック状態: getter */
 public void setChecked(boolean checked) {
  mChecked = checked;

  // チェック状態に応じて画像を変更
  if (mChecked) {
   this.setBackgroundResource(R.drawable.toggle09);
  } else {
   this.setBackgroundResource(R.drawable.toggle01);
  }

  // イベント発火
  if (mListener != null) {
   mListener.onChange(this, mChecked);
  }
 }

 /**
  * コンストラクタ
  * 
  * @param context
  * @param attrs
  */
 public CustomToggleButton(Context context, AttributeSet attrs) {
  super(context, attrs);
  
  // ウィジェットの初期化
  initWidget();
 }

 /**
  * ウィジェットの初期化
  */
 private void initWidget() {
  // 初期値は false に設定
  setChecked(false);

  // クリックされた際に自身の値を変更する
  setOnClickListener(new OnClickListener() {
   public void onClick(View v) {
    // 値を反転
    CustomToggleButton.this.setChecked(!CustomToggleButton.this.isChecked());

    // 実施するアニメーションを選択
    CustomToggleButton.this.setBackgroundResource(
      (CustomToggleButton.this.isChecked() ?
        R.drawable.toggle_on : R.drawable.toggle_off));

    // アニメーション開始
    AnimationDrawable frameAnimation = (AnimationDrawable)
      CustomToggleButton.this.getBackground();
    frameAnimation.start();
   }
  });
 }

 /**
  * イベントリスナの設定
  * 
  * @param listener
  */
 public void setOnToggleChangeListener(OnToggleChangeListener listener) {
  mListener = listener;
 }

 /**
  * 値変更イベントインタフェース
  */
 public interface OnToggleChangeListener {
  public void onChange(View v, boolean isChecked);
 }
}


次は toggle_off.xml。


    
    
    
    
    
    
    
    
    




次は toggle_on.xml。off の順番を逆にしただけ。


    
    
    
    
    
    
    
    
    




最後に main.xml と



    
    

    
    




MainActivity.java
package com.sample.togglesample;

import com.sample.togglesample.CustomToggleButton.OnToggleChangeListener;

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

/**
 * サンプル画面クラス
 */
public class MainActivity extends Activity {

 /** トグルボタンの状態表示テキストビュー */
 private TextView mTxtValue;

 /** カスタムトグルボタンウィジェット */
 private CustomToggleButton mToggleButton;

 /**
  * onCreate
  */
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // トグルボタンが押された際に値をテキストビューに表示
  mToggleButton = (CustomToggleButton) findViewById(R.id.customToggle);
  mToggleButton.setOnToggleChangeListener(new OnToggleChangeListener() {
   public void onChange(View v, boolean isChecked) {
    // トグルボタンの値を表示
    refreshToggleValue();
   }
  });

  // トグルボタンの値を表示
  mTxtValue = (TextView) findViewById(R.id.txtValue);
  refreshToggleValue();
 }

 /**
  * トグルボタンの値を表示
  */
 private void refreshToggleValue() {
  if (mTxtValue != null && mToggleButton != null) {
   mTxtValue.setText(mToggleButton.isChecked() ? "On" : "Off");
  }
 }
}

こんなかんじー。 スワイプして切り替えれるようにしたいけど、まずはタップからやってみました :-)

2012年3月20日火曜日

[Android TIPS] Monkey Runner でアプリ自動実行

Monkey Runner について、これまでに
「一通りやったよ」というような記事があまりなかったと思うので、
今回は Monkey Runner でのアプリ自動実行をご紹介。


Monkey Runner というのは Andorid の操作を予め
決めておいて、それを基に自動実行できる仕組みです。
自動実行の途中でスクリーンショットもとれるので、
テストにはもってこいなわけです。

アプリのレイアウトに依存するところが大きいのですが、
複数の条件を切り替えてテストするような場面や、
長時間実行によるリソース使用量の変化を観測する
場合などに使えるツールです。


自動実行するアプリのサンプル
として、←のような四則演算を
行うアプリを取り上げます

アプリのソースは こちら からどうぞ。

まずは←のアプリをエミュレータで
実行できるように準備します。

また、Monkey Runner は
ボタンなどのウィジェットの操作を
座標を元に行うので、
エミュレータのサイズを 480x800
にします。