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縦書き完成
あとは表示位置がおかしい文字の設定を随時追加していったり、
行末に「、」「。」などがくるよう禁則処理したり、
ルビの表示を実装したりすればいいと思います。
特にルビはデータをどのように保持するか、またパースのやり方を考えないといけないので大変かもしれません。
javascriptからAndroidを呼び出す/Androidからjavascriptを呼び出す
javascript→Android
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!')" />
Android→javascript
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をリリースしました
残念な仕様
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つは消えてくれないようです。
たすけてください
どうにかして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(); } }
無料アプリと有料アプリのプロジェクトを管理する
アプリを作った時に無料アプリと有料アプリと2バージョン作る事ってよくあると思います。
無料アプリと有料アプリに分けたい場合、パッケージ名を変えないといけないのでプロジェクトを2つ立てる必要があります。
その場合、ソースが2重管理になってしまい、リリース後の改修が非常にめんどくださいです。
これはデコ美のバージョンアップでだいぶ苦しめられました…。
これを楽にしようと考えて方法を考えました。
プロジェクト作る
Androidライブラリを使うとソースが1つになり、管理が楽になります。
プロジェクトは合計3つ作ります。
方針
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にそれぞれ定義するので
無料アプリと有料アプリで別のものを使用することができます。
今回は無料アプリにグレーのアイコンを、有料アプリに緑色のアイコンを使いました。
それぞれのアプリを実行しました。
パッケージ名から無料版・有料版の判断をしてそれぞれの文字列を出力しています。
サンプルソース
今回検証用に作ったソースはここにあります
http://code.google.com/p/tomorrowkey/source/browse/#svn/trunk/FreeAndPaymentSourceControl
これで
かなり管理が楽に!
もう何も考えなくていいよう!