2013年12月10日火曜日

[Java / Android TIPS] ファイルを分割・結合する

Android というか Java なんですが、ファイルの分割・結合を行うソースを書いてみました。
プロジェクトは Bitbucket で公開してみました。
内容は Windows のサンプル画像を 200KB 毎に分割し、それを結合する。というものです。

src.jpg


ファイル操作には Apache Commons の Commons IO を使っています。ダウンロードは こちら から。
あと、SD カード上でファイル操作を行うのでマニフェストにパーミッション追加をお忘れなく。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


MainActivity.java
package com.gmail.hyukix.sample_131210_01;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Collection;
import java.util.Locale;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.widget.Toast;

public class MainActivity extends Activity {

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

    // 分割するファイル
    File src = new File(Environment.getExternalStorageDirectory(),
        "Sample/src.jpg");
    if (!src.exists()) {
      String msg = "分割対象ファイルがありません。\n" + src.getAbsolutePath();
      Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
      return;
    }

    // 断片ファイルの拡張子
    String ext = "tmp";

    // 結合先のファイル
    File dst = new File(Environment.getExternalStorageDirectory(),
        "Sample/dst.jpg");

    // ファイル分割 (src.jpg を 200KB 毎に *.tmp へ分割)
    String msg = split(src, ext, 200 * 1024) ? "分割成功!!" : "分割失敗...";
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();

    // ファイル結合 (*.tmp を dst.jpg へ結合)
    msg = combine(dst, ext) ? "結合成功!!" : "結合失敗...";
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
  }

  /**
   * ファイル分割
   * 
   * @param src 分割ファイル
   * @param ext 断片ファイルの拡張子
   * @param size 断片ファイルの最大ファイルサイズ
   */
  private boolean split(File src, String ext, int size) {
    try {
      // 分割ファイルのストリーム
      FileInputStream in = new FileInputStream(src);

      int cnt = 0;
      while (0 < in.available()) {
        // 断片ファイルを作成
        String dirPath = src.getParent();
        String fileName = String.format(Locale.JAPAN, "%08d.%s", ++cnt, ext);
        File dst = new File(dirPath, fileName);
        FileUtils.touch(dst);

        // 断片ファイルへデータ書き出し
        FileOutputStream out = new FileOutputStream(dst);
        IOUtils.copyLarge(in, out, 0, size);
      }

      return true;

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

    return false;
  }

  /**
   * ファイル結合
   * 
   * @param dst 結合ファイル
   * @param ext 断片ファイルの拡張子
   */
  private boolean combine(File dst, String ext) {
    try {
      // 断片ファイルをリスティング
      File dir = dst.getParentFile();
      String[] extensions = { ext };
      boolean recursive = false; // サブディレクトリは対象外
      Collection<File> files = FileUtils.listFiles(dir, extensions, recursive);

      // 断片ファイルを走査
      for (File file : files) {
        // 断片ファイルのデータ
        byte[] data = FileUtils.readFileToByteArray(file);

        // 結合ファイルのストリーム
        boolean append = true; // 追記する
        FileOutputStream out = FileUtils.openOutputStream(dst, append);

        // 断片ファイルを順次結合
        IOUtils.write(data, out);
      }

      return true;

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

    return false;
  }
}


/mnt/sdcard/Sample/src.jpg が存在しない場合は左図が表示されます。
存在する場合は分割、結合を行い、右図が表示されます。



エミュレータで実行した様子が下図です。
結合した dst.jpg が元の src.jpg と同じ内容になっていることを確認できました。



こういうのの定石はあるのかしらん。

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");
  }
 }
}

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