Androidで縦書きを実現する

androidのTextViewは縦書きには対応していません。
縦書きを実現するためには自分で実装するしかありません。
縦書きについてさっぱり知らない状態から実装しました。
twitterでのやりとり

Togetter - 「Android縦書き」 
http://togetter.com/li/92001

誤っている箇所があれば指摘お願いします!
完成はこんな感じです。

参考にしたサイト

字体 - Wikipedia 
http://ja.wikipedia.org/wiki/%E5%AD%97%E4%BD%93

グリフがフォント1文字分、グリフがたくさん集まってフォントという単位になる

禁則処理 - Wikipedia 
http://ja.wikipedia.org/wiki/%E7%A6%81%E5%89%87%E5%87%A6%E7%90%86

行の最初に「、」「。」などの文字が配置される場合、前行の最後に表示するべきという処理
*1

ハイフネーションとは【hyphenation】 - 意味/解説/説明/定義 : IT用語辞典 
http://e-words.jp/w/E3838FE382A4E38395E3838DE383BCE382B7E383A7E383B3.html

英語版禁則処理みたいなもの
行をまたいで英単語を描画する場合ハイフンを使って繋ぐ
*2

ルビ - Wikipedia
http://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%93

ふりがなに使うルビ
*3

[SOLVED] Android: Using Custom Fonts « RussenReaktor's Weblog 
http://russenreaktor.wordpress.com/2010/04/29/solved-android-using-custom-fonts/

自分で用意したフォントを使う例
assetsには1MBまでしか入りません。この例ではassetsにフォントを入れていますが、これは英字フォントのみだからこそできることです。
日本語のフォントが入っていてると1MBなんて余裕で超えちゃうので別途SDカードに入れてあげる必要があります。
他のAndroid縦書きアプリでは大抵アプリインストール後にダウンロードする仕組みになっています。

テキストの描画(FontMetrics) - Android Wiki* 
http://wikiwiki.jp/android/?%A5%C6%A5%AD%A5%B9%A5%C8%A4%CE%C9%C1%B2%E8(FontMetrics)

FontMetrixについて
Canvas.drawText()ではbaseLineを基準に描かれます。

簡易禁則入ってるソース - inuchin104がiPhoneと格闘したメモ 
http://d.hatena.ne.jp/inuchin104/20110121

ちゃんと読めていませんが、
[twitter:@inuchin] さんの作ったDirectXを使ったiPhone用縦書きコードらしいです。
ルビと簡易禁則処理がついているそうです。

Typeface | Android Developers 
http://developer.android.com/reference/android/graphics/Typeface.html

フォントのクラスです。

Paint | Android Developers 
http://developer.android.com/reference/android/graphics/Paint.html

getFontから始まるメソッドを見ておくといいです。

基本的な作り

独自Viewを作り、onDrawでCanvas#drawText()を使って文字を縦に並べていきます。
ひらがなや、漢字などのだいたいの文字の表示は大丈夫ですが、横書き用フォントなので「ー」や「、」や「ぁ」などを使った場合表示が狂った様に見えます。特別な小文字や記号などには別途設定を設けて位置を調整するようにします。
横フォント問題の解決法は縦書きビューワを作られている[twitter:@2SC1815J] さんにもご意見頂きました。

今回は横フォントの位置を調整する方法をとってみようかと思います。

実装

独自フォントのロード
Typeface typeface = Typeface.createFromFile(FONT_PATH);
Paintm paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(FONT_SIZE);
paint.setColor(Color.BLACK);
paint.setTypeface(typeface);
canvas.drawText("こんにちは!", x, y, paint);


このコードを使えば独自フォントを使った文字列を表示する事ができます。
フォントはIPA明朝を使用しました。

IPAフォントのダウンロード || OSS iPedia 
http://ossipedia.ipa.go.jp/ipafont/index.html

こんにちはの文字は横に表示されていますが、縦書きにするために1文字ずつ描画します。

文字サイズ

テキストを縦に並べるためには1文字のサイズを取得する必要があります。
文字サイズはPaint#getFontSpacing()でピクセルサイズを取得する事ができます。
これを使えば固定ピッチで表示する事が可能です。
行間を1文字分、最初の行を1文字落とす設定で走れメロスの冒頭を表示してみました。

この状態でもけっこう様になってます。
しかし「、」「。」「っ」の位置がおかしいです。
横用フォントのため、位置がおかしく表示されます。

文字位置を調整する

文字位置調整の対象はだいたい思いつくもので

  • 記号
  • ひらがな小文字
  • カタカナ小文字
  • アルファベット半角大文字
  • アルファベット半角小文字
  • アルファベット全角大文字
  • アルファベット全角小文字

などがあります。
ひらがな・カタカナ小文字については位置の調整でいいですが、
アルファベット・記号については90度回転する必要があります。
管理が楽になるように設定用のクラスを作っておいてexcelで生成できるようにします。

public class CharSetting {
  /**
   * 文字
   */
  public final String charcter;

  /**
   * 回転角度
   */
  public final float angle;

  /**
   * xの差分
   * Paint#getFontSpacing() * xが足される
   * -0.5fが設定された場合、1/2文字分左にずれる
   */
  public final float x;

  /**
   * xの差分
   * Paint#getFontSpacing() * yが足される
   * -0.5fが設定された場合、1/2文字分上にずれる
   */
  public final float y;

  public CharSetting(String charcter, float angle, float x, float y) {
    super();
    this.charcter = charcter;
    this.angle = angle;
    this.x = x;
    this.y = y;
  }

  public static final CharSetting[] settings = {
      new CharSettings("、", 0.0f, 0.7f, -0.6f), new CharSettings("。", 0.0f, 0.7f, -0.6f),
      new CharSettings("「", 90.0f, -1.0f, -0.3f), new CharSettings("」", 90.0f, -1.0f, 0.0f),
      new CharSettings("ぁ", 0.0f, 0.1f, -0.1f), new CharSettings("ぃ", 0.0f, 0.1f, -0.1f),
      new CharSettings("ぅ", 0.0f, 0.1f, -0.1f), new CharSettings("ぇ", 0.0f, 0.1f, -0.1f),
      new CharSettings("ぉ", 0.0f, 0.1f, -0.1f), new CharSettings("っ", 0.0f, 0.1f, -0.1f),
      // 略
      new CharSettings("V", 90.0f, 0.0f, -0.1f), new CharSettings("W", 90.0f, 0.0f, -0.1f),
      new CharSettings("X", 90.0f, 0.0f, -0.1f), new CharSettings("Y", 90.0f, 0.0f, -0.1f),
      new CharSettings("Z", 90.0f, 0.0f, -0.1f), new CharSettings(":", 90.0f, 0.0f, -0.1f),
      new CharSettings(";", 90.0f, 0.0f, -0.1f), new CharSettings("/", 90.0f, 0.0f, -0.1f),
      new CharSettings("", 90.0f, 0.0f, -0.1f), new CharSettings(":", 90.0f, 0.0f, -0.1f),
      new CharSettings(";", 90.0f, 0.0f, -0.1f), new CharSettings("/", 90.0f, 0.0f, -0.1f),
      new CharSettings(".", 90.0f, 0.0f, -0.1f),
  };

  public static CharSetting getSetting(String character) {
    for (int i = 0; i < settings.length; i++) {
      if (settings[i].charcter.equals(character)) {
        return settings[i];
      }
    }
    return null;
  }
}

文字を描画する前にCharSetting#getSetting()を呼び、設定があったらその設定通りに描画します。
nullが帰ってきた場合、回転も位置調整もせずに描画します。
コードで表すとこんな感じ

CharSettings setting = CharSettings.getSetting(s);
if (setting == null) {
  canvas.drawText(s, x, y, paint);
} else {
  canvas.save();
  canvas.rotate(setting.angle, x, y);
  canvas.drawText(s, x + fontSpacing * setting.x, y + fontSpacing * setting.y, paint);
  canvas.restore();
}

また、CharSettingsをたくさん生成しているところはexcelで管理して生成できるようにしました。

CharSettings作成シート
https://spreadsheets.google.com/ccc?key=0AmA_8rkF2TwodHIyUWpMdldicVhhMGxZWVItNmQ1UkE&hl=ja&authkey=CK_XjqsK

これを使って描画したのがこれ

がんばって調整したのでそれっぽい位置に「、」や「っ」が描画されました。

android縦書き完成

あとは表示位置がおかしい文字の設定を随時追加していったり、
行末に「、」「。」などがくるよう禁則処理したり、
ルビの表示を実装したりすればいいと思います。
特にルビはデータをどのように保持するか、またパースのやり方を考えないといけないので大変かもしれません。

*1:今回は実装していません

*2:今回は実装していません

*3:今回は実装していません

*4:事前にSDカード内にフォントを用意しないとエラーで落ちます

javascriptからAndroidを呼び出す/Androidからjavascriptを呼び出す

javascriptAndroid

javascript interfaceを用意

適当なjavaオブジェクトでおっけー
今回はToastを表示するオブジェクト作りました

import android.content.Context;
import android.widget.Toast;

public class Toaster {

  private Context context;

  public Toaster(Context context) {
    this.context = context;
  }

  public void show(String text) {
    Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
  }
}
WebViewにjavascript interfaceを追加する

WebViewのインスタンスを取得したらさっき作ったオブジェクトを追加します

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // WebView取得
  browser = (WebView) findViewById(R.id.browser);

  // javascript有効化
  browser.getSettings().setJavaScriptEnabled(true);

  // Toastを表示するjavascript interfaceを追加
  // 第一引数がオブジェクトで、第二引数がjavascriptから呼び出すときの名前
  browser.addJavascriptInterface(new Toaster(this), Toaster.class.getSimpleName());

  // assetのindex.htmlを読み込み
  browser.loadUrl("file:///android_asset/index.html");
}
javascriptから呼び出す

あとはこんな感じで呼び出せます

<input type="button" value="click me" onclick="Toaster.show('Hello, World!')" />
実行イメージ


ボタンを押すと…

Toastが表示されました!

Androidjavascript

html側に実行するjavascriptを用意

javascriptの関数書きます
抜粋するとこんな感じ

<head>
<script type="text/javascript">
function change_message(message){
  document.getElementById('message').innerHTML = message;
}
</script>
</head>
<body>
<p id="message"></p>
</body>

alert()でもよかったんですが、android-webviewのalertは表示部分がJavaになって分かりづらいので
今回はテキストを変更する関数を書きました。

Androidから呼び出す

とっても簡単、loadUrlにjavascript書くだけ

browser.loadUrl("javascript:change_message('HogeHoge')");
実行イメージ


メニューを押すと

テキストが変わりました!

Screen Capture Shortcutをリリースしました

リリースしました

新しいアプリをリリースしました。
このアプリを使うと画面のキャプチャを撮ることができます。
しかし動作するのはGalaxyS/GalaxyTabのみです。

無料版


有料版


残念な仕様

Galaxyシリーズにはスクリーンキャプチャを撮るための仕組みがついています。
GalaxySであれば戻るボタンを押しながらホームボタンを、
GalaxyTabであれば戻るボタンを押しながら電源ボタンを押せばスクリーンキャプチャが撮れます。
どちらも戻るボタンを押さねばなりません。
そこでいざスクリーンキャプチャを撮ろうとすると今の画面が取りたいのに前の画面へ戻ってしまいます。
戻る押してるんだから当然ですね…。

そこで

戻るボタンとかホームボタンとか押さなくてもスクリーンキャプチャを呼び出せるアプリを作りました!
使用方法は2通りです。
・notificationバーから呼び出す
・端末を振る(有料版のみ)

これで

いろんな画面を簡単にキャプチャーできますねー。

Launcherからアプリを消したい

だれか助けてください

今書いているアプリでどうしても必要な機能なのですが、どうにも上手くうごきません…、だれか助けてください…

Launcherから消し去りたい

アプリのLauncher表示の切り替えをしたくてPackageManager#setComponentEnabledSettingを使い切り替えるコードを書きました。
PackageManager#setComponentEnabledSettingについてはこちら

Taosoftware: Android Intent呼び出しを自分でコントロール方法
http://www.taosoftware.co.jp/blog/2010/04/android_intent.html

taosoftwareさんではACTION_VIEW/CATEGORY_BROWSABLEのActivityの切り替えをしていますが、ACTION_MAIN/CATEGORY_LAUNCHERのActivityの切り替えをしたらLauncherからの非表示ができるのではないかと考えたわけです。

検証用のコードはこちら

SettingActivity
import android.content.ComponentName;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;

public class SettingActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.setting);

    int state = getPackageManager().getComponentEnabledSetting(new ComponentName(this, SettingActivity.class));
    if (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
      PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("visible_in_launcher_1", true);
    } else {
      PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("visible_in_launcher_1", false);
    }

    state = getPackageManager().getComponentEnabledSetting(new ComponentName(this, SettingActivity.class));
    if (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
      PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("visible_in_launcher_2", true);
    } else {
      PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("visible_in_launcher_2", false);
    }
  }

  @Override
  protected void onResume() {
    super.onResume();
    PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);
  }

  @Override
  protected void onPause() {
    super.onPause();
    PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
  }

  @Override
  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    int newState;
    if (sharedPreferences.getBoolean(key, true)) {
      newState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
    } else {
      newState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
    }
    String packageName = getPackageName();
    ComponentName componentName;
    if (key.equals("visible_in_launcher_1")) {
      componentName = new ComponentName(packageName, packageName + ".SettingActivity");
    } else {
      componentName = new ComponentName(packageName, packageName + ".SettingActivityAlias");
    }
    PackageManager packageManager = getPackageManager();
    packageManager.setComponentEnabledSetting(componentName, newState, PackageManager.DONT_KILL_APP);
  }
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="jp.tomorrowkey.android.visibleinlauncherapp"
  android:versionCode="1"
  android:versionName="1.0">
  <application
    android:icon="@drawable/icon"
    android:label="@string/app_name">
    <activity
      android:name=".SettingActivity"
      android:label="App1"
      android:enabled="true">
      <intent-filter>
        <action
          android:name="android.intent.action.MAIN" />
        <category
          android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity-alias
      android:name=".SettingActivityAlias"
      android:targetActivity=".SettingActivity"
      android:label="App2"
      android:enabled="true">
      <intent-filter>
        <action
          android:name="android.intent.action.MAIN" />
        <category
          android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity-alias>
    <receiver
      android:name=".PackageChangedReceiver">
      <intent-filter>
        <action
          android:name="android.intent.action.PACKAGE_CHANGED" />
        <data
          android:scheme="package" />
      </intent-filter>
    </receiver>
  </application>
  <uses-sdk
    android:minSdkVersion="3" />
</manifest> 

ちょっとわけあってsetComponentEnabledSettingでenableを切り替えるActivityを2つにしています。
また、setComponentEnabledSettingを使って変更をするとPACKAGE_CHANGEDがブロードキャストされるので、ブロードキャストされたことを分かりやすく見せるためにToastを表示するようにしています。

package jp.tomorrowkey.android.visibleinlauncherapp;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class PackageChangedReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "receive package changed", Toast.LENGTH_SHORT).show();
  }
}

動かない…

まず、Launcherの状態がこちら

アプリを起動するとこんな画面が表示されます。

1のチェックボックスを外すと

PACKAGE_CHANGEDが受信され

LauncherからApp1のリンクが消えます。

思ったように動きます。

ここからが問題です。
この状態でApp2のチェックボックスを外すと…

PACKAGE_CHANGEDが受信され

Launcherから消えない…

なぜか消えません…
ちなみに起動しようとしてみると

「アプリがインストールされてません」って言われます…
どうも、最後の1つは消えてくれないようです。

検証Homeアプリ
  • GalaxyS純正Launcher
    • 消えない
  • HT-03A純正Launcher
    • 消えない
  • ADWLauncher
    • 消えない
  • Xperia(1.6)純正Launcher
    • 消える
  • Xperia mini pro(1.6)純正Launcher
    • 1つも消えない

たすけてください

どうにかしてLauncherから消す方法はないでしょうか…。
知っている方おしえてください…

ある程度時間が経過したらプログレスダイアログを表示する

最初からプログレスを表示せずにある程度時間が経ったらプログレスダイアログを表示します。
処理時間がまちまちな時に使えるんじゃないかなと思います。
onPostExecuteでプログレス非表示/メッセージキャンセルのif文がこんなので大丈夫か不安です。
初めてメッセージ使った。

AsyncTaskソース

処理が2秒以上掛かる場合はプログレスを表示します。

class WaitTask extends AsyncTask<Integer, Void, Integer> {

  /* プログレスが表示されるまでの閾値 */
  private static final int PROGRESS_DELAY = 2000;

  /* Message識別*/
  private final int MESSAGE_WHAT = 100;

  private Context context;
  private ProgressDialog progressDialog = null;
  private Handler handler;

  public WaitTask(Context context) {
    this.context = context;
    handler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
        progressDialog = new ProgressDialog(WaitTask.this.context);
        progressDialog.setMessage("please wait");
        progressDialog.show();
      }
    };
  }

  @Override
  protected Integer doInBackground(Integer... params) {

    Message msg = new Message();
    msg.what = MESSAGE_WHAT;
    handler.sendMessageDelayed(msg, PROGRESS_DELAY);

    try {
      //何か時間が掛かる処理
      Thread.sleep(params[0]);
    } catch (InterruptedException e) {
      Log.d("DelayedProgress", e.getMessage(), e);
    }
    return params[0];
  }

  @Override
  protected void onPostExecute(Integer result) {
    if (progressDialog != null && progressDialog.isShowing()) {
      progressDialog.dismiss();
    } else {
      handler.removeMessages(MESSAGE_WHAT);
    }
    Toast.makeText(context, String.format("%d second has passed", result), Toast.LENGTH_LONG).show();
  }
}

ドット絵を描くアプリを作ったよ!

ドット絵を描くアプリを作ったよ

.Picというアプリを作りました!
このアプリを使うとデコメを自作することができます。


DotPicFree


無料です。
使える色が少ないです。

DotPic


300円です。
色が自由に選べます。

無料アプリと有料アプリのプロジェクトを管理する

アプリを作った時に無料アプリと有料アプリと2バージョン作る事ってよくあると思います。
無料アプリと有料アプリに分けたい場合、パッケージ名を変えないといけないのでプロジェクトを2つ立てる必要があります。
その場合、ソースが2重管理になってしまい、リリース後の改修が非常にめんどくださいです。
これはデコ美のバージョンアップでだいぶ苦しめられました…。
これを楽にしようと考えて方法を考えました。

プロジェクト作る

Androidライブラリを使うとソースが1つになり、管理が楽になります。
プロジェクトは合計3つ作ります。

  • AndroidPayApp(Androidアプリ)
    • 有料アプリ
    • package:jp.tomorrowkey.android.androidpayapp
  • AndroidFreeApp(Androidアプリ)
    • 無料アプリ
    • package:jp.tomorrowkey.android.androidfreeapp
  • AndroidAppCore(Androidライブラリ)
    • 実装コード
    • package:jp.tomorrowkey.android.androidcoreapp

方針

AndroidAppCodeにすべてのコードを書きます。
AndroidPayAppとAndroidFreeAppの2つのアプリはAndroidAppCoreを参照します。
AndroidPayAppとAndroidFreeAppの2つのプロジェクトには一切ソースは置きません。

AndroidManifest.xml

AndroidPayAppとAndroidFreeAppのAndroidManifestにはAndroidAppCoreのActivityの定義を書きます。
今回は1つのActivityのみなので以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="jp.tomorrowkey.android.androidpayapp"
  android:versionCode="1"
  android:versionName="1.0">
  <application
    android:icon="@drawable/icon"
    android:label="@string/app_name">
    <activity android:name="jp.tomorrowkey.android.androidlib.MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
  <uses-sdk android:minSdkVersion="4" />
</manifest> 

掲載したのはAndroidPayAppのAndroidManifestです。
AndroidFreeAppのAndroidManifestはほぼ一緒なので載せません。

これでアプリからライブラリに定義されているActivityが呼び出せます。

無料アプリと有料アプリの判断

無料アプリは機能に制限をもたせないといけないので、無料アプリと有料アプリの判断をするコードが必要です。
Context#getPackageName()で有料アプリ、無料アプリそれぞれのパッケージ名が返ってくるのでそれで判断します。
Utilクラスでも作っておきましょう。

import android.content.Context;

public class Util {
  private static final String PAY_APP_PACKAGE_NAME = "jp.tomorrowkey.android.androidpayapp";
  public static boolean isPaymentApp(Context context) {
    String packageName = context.getPackageName();
    return PAY_APP_PACKAGE_NAME.equals(packageName);
  }
}

動作


アイコンとアプリ名はAndroidManifestにそれぞれ定義するので
無料アプリと有料アプリで別のものを使用することができます。
今回は無料アプリにグレーのアイコンを、有料アプリに緑色のアイコンを使いました。


それぞれのアプリを実行しました。
パッケージ名から無料版・有料版の判断をしてそれぞれの文字列を出力しています。

これで

かなり管理が楽に!
もう何も考えなくていいよう!