Quantcast
Channel: kenji matsuokaのニッキ
Viewing all 342 articles
Browse latest View live

正しいMacBook Air/Pro用ACアダプターケーブルの巻き方

$
0
0
MacBook Air/ProのACアダプターってオシャレなんですが結構ケーブルをダメにしている人を聞きます。
と思っていると自分もダメに仕掛けたので正しいケーブルの巻き方を考えてみました。
MacBook Air/ProのACアダプターって真四角でおしゃれ感漂います。

ケーブルを引っ掛けるようのフックが付いているのでケーブルを簡単にまとめられるのも便利。


間違った巻き方

間違ったというか、多分公式の巻き方で殆どの人がこの方法で巻いていると思います。

フックにそのままぐるぐると巻きつけて最後にピンをつければ完成。
コンパクトにまとまっていて綺麗です。 が

この巻き方のダメなところは解くとすぐに分かります。
magsafe部分を掴んだままケーブルをほどいていくと

なんだかおかしなことになってきました。

途中でケーブルが絡まって変な状態になってしまいました。
これはケーブルが巻いているのが原因
巻いている時には気づきにくいですが、magsafe部分を見ていると巻いている時にくるくるとmagsafe部分が回っているのがわかります。

ケーブルが回っていると何が悪いかというと、回っている内側と外側で長さが変わってしまうこと。
内輪差のように内側は短く、外側は長くなるので内側のケーブルは押し付けられ、外側のケーブルは引っ張られる力が働きます。
ケーブル自体はある程度伸縮性があるように作られているのですが、限界はあるので、この状態が続くと押し付けられた方は外へ飛び出し、引っ張られた方は内側に引っ張られ内側のケーブルが中で丸まってしまうことが有ります。

正しい巻き方

そこで、内側と外側を途中で入れ替えてやります。
まずは反時計回りで普通に巻き始めて

3周(Airの場合)巻いたところでたすき掛けにして時計回りにします。

同様に3周巻いてまたたすき掛けにして反時計回りに

3周巻いたらまた時計回りに

最後にピンで止めます。


では同様に解いてみましょう。
ぴよーん

巻くことなくきれいに解けました。


なお3周ごとに入れ替えるのはMacBookAirの場合で、MacBookProではACアダプターの大きさが違うので回数が変わってきます。
特に15インチはacアダプターが大きいのでトータルの巻く回数が少ないです。(それ故普通の巻き方でもよれにくい)
そのため15インチの場合は2周程度が良さそうです。


大事なのは時計回りと反時計回りの回数をほぼ同じにすること、片方の巻き方を続けず程よく入れ替えることです。

原理

電気に詳しい人なら既に気づいていると思うのですが、これは8の字巻きと呼ばれる巻き方の応用です。
8の字巻では1周毎に時計回りと反時計回りを入れ替えますが、MacBookのACアダプターでは一周ごとに入れ替えているとたすき掛け部分のケーブルが増えすぎてフックに収まらなくなってしまいます。
そこで巻く回数を増やしてたすき掛け部分を少なくしています。

途中で内側と外側が入れ替わるので押し付けられていたほうが次では引っ張られお互いに打ち消し合うのでケーブルに強いテンションをかけることなくケーブルを巻くことが出来ます。

なお、この方法の要領でイヤホンを絡まりにくくすることも出来ます。
そちらもいつかご紹介しようと想います。

Android 6.0のRuntime Permissionに対応する

$
0
0
Android 6.0ではRuntime Permissionという概念が取り入れられました。
それまでのアプリでは動作に問題が出ることが有ります。

Runtime Permissionとは

Androidではカメラやネットワークなど一部の機能を使用するには、ManifestにPermissionを記載して、使用することを明示的に宣言しないと使えない機能があります。
これにより、ユーザーはそのアプリがどのような機能を使用しているかを把握することが出来ます。

PermissionはAndroid6.0未満では全てインストール前に確認するという方式が取られていました。
ユーザーがPermissionを許可しなかった場合アプリをインストール出来ません。
しかし、この方式はいくつかの問題を含んでいます。
  • 多くのユーザーはインストール前にそのアプリがどのPermissionをどうやって使うのか把握しづらく、ユーザーはPermissionに何が宣言されていようとそれが妥当であるか判断できない。
  • 一度許可したアプリは永続的に許可され続けるのでユーザーによるPermissionチェックが適切に働きにくい。
  • Permissionが変更されるとユーザーの手動アップデートが必要となるため、Permissionを追加したバージョンアップでアップデートがなかなか行ってもらえなくなったり、それを防ぐために予め片っ端からPermissionを宣言しておくなどということが発生していた。

それらを解決するためにAndroid6.0ではRuntime Permissionという仕組みが取り入れられました。
Runtime Permissionではカメラやアドレス帳へのアクセスと言ったリスクの高いPermissionについてインストール時には許可を求めず、アプリ実行中にアプリ内で許可を求めます。

これにより、Permissionを必要になったタイミングで求めることができるので、ユーザーが何故そのPermissionが必要なのか理解しやすくなり、ユーザーはあとから、そのアプリがどのような機能を使っているかを見ることが出来てPermissionを変更することも出来ます。
Permisionを追加しても、自動的にアップデートが行われるようになり、古いアプリを使い続けることが減ると思われます。

Runtime Permissionの動き

Runtime Permissionがどのように動くは、TargetAPIの指定により変わってきます。
TargetAPIが22以前の場合は従来のアプリのようにインストール時にPermissionの一覧が表示されユーザーが全てのPermissionを許可することでインストールが可能となります。
これにより、インストール直後はアプリが使う全てのPermissionが許可 状態となっています。

TargetAPIが23以上の場合は標準的なPermissionについては今までどおりインストール時に行われますが、センシティブなPermissionについてはインストール時にはチェックされず、そのままアプリがインストールされます。
TargetAPIが22以前の場合と違い、この時点ではセンシティブなPermissionは拒否状態となっています。


Target SDKが22以前だったとしてもインストール時に許可を求めることで初期値が許可になるだけという点に注意してください。

target SDKに関係なくAndroid6.0ではインストール後個別にPermissionを拒否することができます。
その場合対応していないアプリでは例外で落ちてしまいます。(Permissionを拒否するときに警告ダイアログは出ます。)
そのため、Runtime Permissionの対応が面倒なのでTargetSDKを22のままにしておくというのはあまり良い対応ではないです。

Target SDKが22以下のアプリにたいしてPermissionを拒否した時に表示されるダイアログ。
警告はしてくれるがそれに従うかはユーザーの自由。
「このアプリはAndroidの以前のバージョンを対象としています。権限を許可しないと、意図したとおりに動作しなくなる可能性があります。」

Runtime PermissionはAndroid6.0以降でのみ動作します。
Android5.1以下ではtargetSDKのバージョンによらずすべてのPermissionが従来通りインストール前に確認されます。

いつ許可を求めるか

アプリがそのPermissionをどの程度必要としているかによって許可を求めるタイミングを変えることで利便性を改善できます。

Permissionが無いとアプリが成立しない場合

アプリを開始時にPermissionの許可取得リクエストを実行します。
インストール後にもPermissionを拒否できるので、実行時のチェックも必要ですが、いざ利用したいという時に個別にパラパラと許可を求めると、ユーザーにとって煩わしく感じるおそれがあるため最初の段階で許可を求めます。
例えば画像編集アプリであればストレージへのアクセスは必要なので起動時にPermissionを求めます。

Permissionが無くてもアプリが成立する場合

アプリの一部機能だけでPermissionを使用している場合など、Permissionがなくてもアプリとして成立するのであれば、起動時にはPermissionの要求を行わずに、Permissionが必要な機能を使用する段階で許可を求めるダイアログを表示します。
例えば画像編集アプリの場合、カメラへのアクセスは、写真を撮って加工したい人には必要ですが、既に撮った写真を加工したい人にとっては不要です。
このような場合はカメラ機能を押したタイミングでPermissionを求めるようにすることで、カメラ機能を使わないユーザーに不要な気を使わせる必要がなくなります。

ユーザーのアクションを主体とせずにPermissionが必要な場合

SMSの受信のようにユーザーがアクションしていないタイミングで必要となるPermissionは、たとえPermissionがなくてもアプリが成立したとしても、Permissionを求めるタイミングが他にないため起動時に許可を求めます。

どうやって許可を求めるか

そのPermissionを要求する理由が明確かによって許可の求め方を変えます。

ユーザーがPermissionを必要とする理由が明確である場合

単にPermissionを要求するダイアログを表示します。
カメラボタンを押した時にカメラのPermissionをリクエストするのはユーザーにとってわかりやすく、単にPermissionを要求するダイアログで事足ります。

Permissionを必要とする理由がわかりにくい場合

Permissionが必要な理由を説明します。
例えばユーザー認証を行うためにSMSとの自動連携を行っている場合、SMSへアクセスできるPermissionが必要ですが、ユーザーはSMSへのアクセス権が何故必要なのか理解できない可能性があります。
このような場合にはユーザー認証にSMSを使用しており、SMSへのアクセスに許可しないと正しく認証ができない旨を伝えて許可を求めます。

拒否されたら

拒否された場合にどのように振る舞うかは、いくつか考えられます。

Permissionがないとアプリの本質的な機能を使用することができなくなる場合

Permissionの許可を求める画面を全面に表示し、それ以上先に進めなくします。
これによりユーザーに強くPermissionを求めることが出来ます。

Permissionがなくてもアプリとして機能するが、Permissionが拒否されることがユーザーの一時的な気の迷いだと思える場合

再度該当の機能を実行するタイミングで、なぜそのPermissionが必要なのかを説明を表示して、再度承諾を求めるダイアログを表示します。
ユーザーはすぐにPermissionを有効にすることが出来ます。

Permissionがなくてもアプリとして機能し、かつユーザーが明示的にPermissionを拒否し続けると思われる場合

対象のボタンを無効化して、拒否された機能を使用できなくします。
これにより、ユーザーは2度とPermissionを求めるダイアログでイライラしなくて良くなります。
この場合、ユーザーが設定から明示的にアプリに許可を付け直す必要があります。

Runtime Permissionの対象となるPermission

Runtime Permissionの対象となるPermissionは次のとおりです。
READ_CALENDAR カレンダーの読み込み
WRITE_CALENDAR カレンダーの書き込み
CAMERA カメラ機能
READ_CONTACTS 連絡先の読み込み
WRITE_CONTACTS 連絡先の書き込み
GET_ACCOUNTS ユーザーが使用しているアカウントの取得
ACCESS_FINE_LOCATION 詳細な位置
ACCESS_COARSE_LOCATION 大まかな位置
RECORD_AUDIO オーディオの録音(マイク)
READ_PHONE_STATE 電話の状態を取得する
CALL_PHONE 電話をかける(直接発信するのではなく通話アプリをIntentで呼び出す場合は不要)
READ_CALL_LOG 通話履歴を取得する
WRITE_CALL_LOG 通話履歴を書き込む
ADD_VOICEMAIL ボイスメールを追加する
USE_SIP SIP( Session Initiation Protocol)を使用する
PROCESS_OUTGOING_CALLS 通話発信時のIntentを補足する
BODY_SENSORS 心拍数などの身体センサーを使用する
SEND_SMS SMSを送信する
RECEIVE_SMS SMSを受信する
READ_SMS SMSを読む
RECEIVE_WAP_PUSH WAP(Wireless Application Protocol)PUSHを受信する
RECEIVE_MMS MMSを受信する
READ_EXTERNAL_STORAGE 外部ストレージから読み込む
WRITE_EXTERNAL_STORAGE 外部ストレージに書き出す

上記のPermissionを使っている場合Android6.0以上の端末ではRuntime Permission扱いとなり、対応が必須となります。(Target SDKを22以下とすることで、とりあえず動かし続けることは可能です。 しかしながら、その場合ユーザーが明示的にPermissionを拒否すると例外で落ちます。)

Permissionとどう付き合うか

Permissionを許可しないユーザー

今回の変更でPermissionに対するユーザーの考え方が変わってくる可能性があります。
これまではインストール時に漠然と表示していたため多くのユーザーがPermissionを意識せずに許可していました。
一方で今後は明確にユーザーを説得する必要が出てくるので、かなりの割合でPermissionを許可しないユーザーが出てくると思います。

Permissionを減らす

まず第一に利用するPermissionを減らす事を考えます。
例えば電話を発信する場合、アプリ内から直接発信する場合はPermissionが必要ですが、番号がセットされた発信画面を表示するまでならPermissionは必要ありません。
ユーザーが明示的に通話ボタンを押す必要が出てきますが、むしろそちらのほうがユーザーフレンドリーといえる場合もあるはずです。
Intentが必要な電話発信

Uri phoneNumber = Uri.parse(“tel:0123456789″);
Intent callIntent = new Intent(Intent.ACTION_CALL,phoneNumber);
startActivity(callIntent);


Intentを必要としない電話発信

Uri phoneNumber = Uri.parse(“tel:0123456789″);
Intent callIntent = new Intent(Intent.ACTION_DIAL,phoneNumber);
startActivity(callIntent);

同様に、現在地を表示する場合にはPermissionが必要ですが地図アプリをIntentで開けばPermissionが必要なくなります。

許可されない状態で動作する

Permissionを許可しないユーザーもユーザーとして取り込むにはPermissionが許可されていない状態で出来るだけ多くの機能が動作できるような作りにする必要があります。

対応方法

対応に必要な手順は3つあります。
1.Permissionが許可されているか確認する。
2.Permissionが許可されていない場合許可を求める。
3.Permissionが許可・拒否された時の処理を追加する。

1.Permissionが許可されているか確認する。

Permissionの許可を求めるにはContextCompat#checkSelfPermission()を使用します。
第1引数にContext,第2引数に確認したいPermissionを表す文字列を渡します。
Permissionを表す文字列は android.Manifest.permissionから選択します。存在しないPermissionを指定すると実行時例外が発生するため注意してください。

通常はManifest.permission.CAMERAのようにManifestをimportして指定することが出来ますが、GCMを使っている場合には別のManifestクラスが自動生成されるため明示的にandroid.Manifestをimportするか、android.Manifest.permission.CAMERAのように完全修飾子を指定します。

戻り値はint型で、許可されている場合は、PackageManager.PERMISSION_GRANTED
拒否されている場合はandroid.content.pm.PackageManager.PERMISSION_DENIEDが返ります。

CAMERAが許可されているかを確認する場合の例

if (ContextCompat.checkSelfPermission(
this,android.Manifest.permission.CAMERA)== PackageManager.PERMISSION_GRANTED){
// 許可されている時の処理
}else{
// 拒否されている時の処理
}

2.Permissionが許可されていない場合許可を求める。

許可されていないことが分かった場合は ActivityCompat#shouldShowRequestPermissionRationale()を呼びます。
このメソッドはユーザーがPermissionを明示的に拒否したかどうかを返します。
引数はContextCompat#checkSelfPermission()と同様に、Contextと確認するPermissionを表す文字列です。
ユーザーが明示的に拒否した場合はtrue、ユーザーがまだ許可していない場合はfalseが返ります。
trueが帰ってきた場合、ユーザーにPermissionが何故必要なのか説明して説得して再度許可を求めるか、ボタンを無効にするなどの処理を行います。
falseが帰ってきた場合、ユーザーはまだ判断を行っていないので許可を求めるダイアログを表示します。

許可を求めるには
ActivityCompat#requestPermissions()
を使用します。
引数は
第1引数にContext
第2引数にPermissionを表す文字列の配列
第3引数にコールバックで受け取るint型の数字(任意の数字)
を指定します。

第2引数がcheckSelfPermission()やshouldShowRequestPermissionRationale()と違って配列なのに注意してください。
複数の文字列を渡すことでPermissionの許可を一度に求めることが出来ます。
ここで指定するPermissionはAndroidManifestに宣言したPermissionでなくてはいけません。
もしAndroidManifestに宣言した以外のPermissionを指定すると
「問題が発生したため、パッケージインストーラーを終了します。」というわかりにくいエラーが表示されアプリが終了します。


Cameraが許可されていない場合、ユーザーが明示的に拒否したかどうかで処理を分ける例

if (ContextCompat.checkSelfPermission(
this,android.Manifest.permission.CAMERA)== PackageManager.PERMISSION_GRANTED){
// 許可されている時の処理
}else{
//許可されていない時の処理
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.CAMERA)) {
//拒否された時 Permissionが必要な理由を表示して再度許可を求めたり、機能を無効にしたりします。
} else {
//まだ許可を求める前の時、許可を求めるダイアログを表示します。
ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.CAMERA}, 0);

}
}


Permissionがない場合このようなダイアログが表示されます。
「写真の撮影と動画の記録を許可しますか?」
複数のPermissionを指定していた場合は1/2のように許可するPermissionがいくつあるかも表示されます。

3.Permissionが許可・拒否された時の処理を追加する

ダイアログでPermissionが許可・拒否された時に処理を行うにはActivityあるいはFragmentのonRequestPermissionsResult()をOverrideします。
引数として、
第1引数にActivityCompat#requestPermissions()の第3引数で指定した値
第2引数にActivityCompat#requestPermissions()の第2引数で指定した値
第3引数に第2引数と1対1になるかたちでユーザーがPermmisionを許可したかどうかがintで渡されます。

後はそれに合わせて、許可された場合は後続の処理を行い、拒否された場合はボタンを無効にするとか、説得するためのメッセージを表示するなど必要な処理を行います。

カメラのPermissionが許可されたかどうかを確認する例

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case 0: { //ActivityCompat#requestPermissions()の第2引数で指定した値
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//許可された場合の処理
}else{
//拒否された場合の処理
}
break;
}
}
}


これによりRuntime Permissionに対応できます。

Android6.0について

Runtime PermissionはAndroidのバージョンアップ史上、最も多くのAPKに影響を与えるバージョンアップであるように思います。
targetAPKを変更しないことで暫定回避はできるものの、例外で落ちるリスクを考えると、できるだけ早めに対応しておくべきでしょう。


Android6.0の正式版は今週中にも配信予定です。

Androidといえば正式版のアップデートが発表されても、すぐに対応するのは開発者向けのNexusだけで、それ以外の端末のアップデートは遅れる印象でしたが、Android5.0からDeveloper Previewが配信されるようになり、正式版から採用への間隔が短くなっています。
(噂レベルですが、HTCは今月中に6.0対応のスマートフォンを発売すると言われています。)


もし、対象のPermissionを使っているアプリが既に存在する場合は早めに対応を行ってテストしてください。

既存アプリに影響するAndroid 6.0での変更点

$
0
0
Android6.0では多くのAPIに変更が加わりました。
もっとも大きな変更点はRuntime Permissionでしょうが、それ以外にも様々な変化が含まれています。
ここでは、既にAndroidアプリを公開している人向けに、既存のアプリにどのような影響が及ぶかについて説明されています。

原文

Android 6.0(API level 23)では新機能や能力とともに、システムやAPIの振る舞いに幾つかの変更が加えられています。
ここでは、理解しておかなくてはいけないアプリに影響を及ぼす重要な変化のハイライトを紹介します。

もし、あなたがこれまでAndroid向けにアプリを公開しているなら、プラットフォームの変更にともなってアプリが影響を受けることに注意してください。

Runtime Permission

今回のリリースでは新しいPermissionモデルが導入され、ユーザーは実行時に直接アプリのPermissionを管理することが出来るようになりました。このモデルによりユーザーはPermissionの確認と管理を行いやすくなります。
開発者の利点として新規インストールと自動アップデートが合理化されます。
ユーザーはインストールされたアプリに対してPermissionを個別に許可したり拒否することが出来ます。

アプリがAndroid6.0(API level23)以上をターゲットとしている場合は、実行時にPermissionの要求を行っていることを確認してください。Permissionが許可されているかどうかを確認するにはcheckSelfPermission()を呼びます。Permissionを要求するには新しいrequestPermissions()を呼びます。また、Android6.0(API level 23)をターゲットとしていない場合もアプリが新しいPermission モデルで正しく動作することをテストしておく必要があります。

アプリで新しいPermissionモデルをサポートするための詳細はWorking with System Permissionss
(訳注:あるいはAndroid 6.0のRuntime Permissionに対応する(日本語))を確認してください。アプリにどのような影響があるかを確認する方法はPermissions Best Practicesを見てください。

DozeとApp Standby

このリリースでは端末やアプリが使われていない時の省電力機能が最適化されました。この機能はすべてのアプリに影響を及ぼします。そのためアプリでこの新しいモードをテストしてください。

Doze

ユーザーが一定時間端末を充電中でなく、画面がOFFのまま、放置された状態であるなら、デバイスはDozeモードに入ります。このモードはシステムをできるだけスリープ状態にしておきます。
このモードに有る時デバイスは定期的に短期間リジュームします。その間アプリは同期を行い、システムは待ち状態の様々な操作を行います。

App Standby

App Standbyはユーザーがアプリを一定期間活発に使っていない場合に、アプリが待機状態であると判定します。
システムは端末が充電されていない時、待機状態のアプリに対して通信を無効化し、同期やジョブを休止します。

省電力機能の変更についてより詳細を学ぶにはAndroid 6.0 Changes | Android Developersを読んでください。

Apache HTTP Clientの削除

Android6.0ではApache HTTP Clientをサポートしなくなりました。もし、アプリでこのClientを使用しておりかつ、対象がAndroid2.3(API level9)以上であるならば、代わりにHttpURLConnectionクラスを使用してください。このAPIはtransparent圧縮とレスポンスのキャッシュによりネットワーク使用量を減らし、電力消費量を最小化してくれるため、より効率的です。Apache HTTP API群を使い続けるのであれば、最初にbuild.gradleファイルへ 以下のcompile-time dependencyを宣言する必要があります。

android {
useLibrary 'org.apache.http.legacy'
}


BoringSSL

AndroidはOpenSSLからBoringSSLライブラリに移行しています。もし、アプリ内でNDKを使用しているならば、libcrypto.so や libssl.so のようなNDK APIの一部でない暗号化ライブラリにリンクしてはいけません。
これらのライブラリは公開されたAPI群ではなく、リリースや端末に寄っては警告なしに変更されたりなくなってしまうことがありえます。くわえて、脆弱性によりセキュリティの問題に直面するかもしれません。
代わりに、JNI経由でJavaの暗号化APIを呼ぶか静的リンクで暗号化Libraryを呼ぶようにネイティブコードを修正してください。

ハードウェア識別子へのアクセス

ユーザーのデーターを保護するために、このリリースではWi-FiとBluetoothAPI群を使うアプリにおいて、プログラムでデバイスのローカルハードウェア識別子へアクセスすることが出来なくなりました。
WifiInfo.getMacAddress()BluetoothAdapter.getAddress()は常に02:00:00:00:00:00を返すようになります。

BluetoothやWi-Fiスキャンによって近くのアクセス可能なハードウェア識別子へアクセスするにはACCESS_FINE_LOCATIONACCESS_COARSE_LOCATIONpermissionが必要です。
WifiManager.getScanResults()
BluetoothDevice.ACTION_FOUND
BluetoothLeScanner.startScan()
メモ:Android6.0(API level23)がWi-FiやBluetoothスキャンを始めた時、外部デバイスに対してはランダムなMACアドレスが使われます。

Notifications

このリリースではNotification.setLatestEventInfo()メソッドが削除されます。Notificationを作るにはその代わりにNotification.Builderクラスを使用してください。Notificationを繰り返し更新するにはNotification.Builderインスタンスを再度利用します。
更新されたNotificationのインスタンスを取得するにはbuild()を呼びます。

adb shell dumpsys notificationコマンドはnotificationテキストを出力しなくなりました。Notificationオブジェクト内のテキストを出力するには代わりにadb shell dumpsys notification --noredactコマンドを使用します。

AudioManagerの変更

AudioManagerクラスを使って特定のストリームの音量を直接変更したりミュートにすることは出来なくなります。
setStreamSolo()メソッドは非推奨となりました。かわりにrequestAudioFocus()を呼ぶ必要があります。同様に、setStreamMute()メソッドは非推奨となりました。代わりにadjustStreamVolume()ADJUST_MUTEADJUST_UNMUTEを渡してください。

テキストの選択

ユーザーがアプリ内でテキストを選択する時、floating toolbar内で切り取り、コピー、貼り付けといったテキスト選択アクションを表示できます。ユーザーとの対話はcontextual action barと同様に、Enabling the contextual action mode for individual views内で記載して実装します。

テキスト選択のFloating toolbarを実装するためには、既存のアプリに以下の変更を加えます。
1.ViewあるいはActivityActionModeを呼ぶために、startActionMode(Callback) の代わりにstartActionMode(Callback, ActionMode.TYPE_FLOATING).を呼びます。

2.既存のActionMode.Callbackの実装の代わりにActionMode.Callback2を継承します。

3.Rectオブジェクト(矩形で選択されたテキストなど)と同調するために、onGetContentRect()メソッドをOverrideします。

4.選択された矩形がもう必要なく無効にしたい時、無効にする要素がこれ一つであるならinvalidateContentRect()メソッドを呼びます。

Android Support Library22.2ではfloating toolbarの以前のバージョンへの互換性は提供されず、AppCompatはデフォルトとしてActionModeオブジェクト上でコントロールされるので注意してください。このため、floating toolbarは表示されなくなります。
AppCompatActivityActionModeをサポートするにはgetDelegate()を呼び、戻り値のAppCompatDelegateオブジェクトに対して、setHandleNativeActionModesEnabled()メソッドを引数にfalseをセットして呼んでください。
この呼出ではActionModeオブジェクトのControlが返ります。Android6.0(API level23)が動作するデバイスではActionBarか、floating toolbarモードをサポートすることが出来ます。Android5.1(API level22)以下の端末ではActionBarモードのみがサポートされます。


ブラウザブックマークの変更

このリリースではグローバルブックマークのサポートが削除されました。android.provider.Browser.getAllBookmarks()とandroid.provider.Browser.getAllBookmarks()メソッドが削除され、関連してREAD_HISTORY_BOOKMARKSとWRITE_HISTORY_BOOKMARKSのPermissionも削除されました。もし、アプリのTargetSDKがAndroid6.0()API level23
以上の場合global provicerやbookmark permissionによりブックマークにアクセスすることは出来ません。代わりにアプリ内のデータとしてブックマークを保持してください。

Android Keystoreの変更

このリリースではAndroid Keystore providerのDSAがサポートされなくなりました。ECDSAは引き続きサポートされます。

セキュアロックスクリーンが無効化されたりリセット(例えばユーザーやデバイス管理者により)された時、暗号化が必要でないキーは削除されません。暗号化が必要なキーは削除されます。

Wi-Fiとネットワークの変更

このリリースではWi-FiとネットワークのAPI群の動きについて以下の変更が加わっています。

・アプリは自身で作成したWifiConfigurationの状態を変更することが出来ます。ユーザーや他のアプリが作成したWifiConfigurationを編集したり削除することは許可されません。

・これまで、アプリでenableNetwork()を使用してdisableAllOthers=trueをセットし特定のWi-Fiにデバイスを強制的に接続した場合、デバイスは携帯網などの他のネットワークから切断されていました。このリリースでは他のネットワークから切断されなくなります。
もし貴方のアプリがtargetSdkVersionが20以下であれば選択したWi-Fiネットワークが固定されます。
targetSdkVersionが21以上の場合、openConnection()bindSocket()、新しいbindProcessToNetwork()メソッドのようなマルチネットワークAPI群を使用することで、確実に選択したネットワークでネットワーク転送を行うことが出来ます。

カメラサービスの変更

このリリースではカメラサービスのリソース共有モデルについて、それまでの早い物順のアクセスモデルから優先度に基づいて処理されるモデルに変更されます。 サービスは以下の変更を含みます。

・カメラサブシステムリソースへのアクセス(カメラデバイスを開いたり設定したりを含む)はクライアントアプリケーションプロセスの優先度に基づき与えられます。アプリがユーザーに見えているか最前面のActivityの場合、通常カメラリソースを獲得して使用するための高い優先度が与えられます。

・優先度の低いアプリでアクティブなカメラクライアントは、優先度が高いアプリがカメラを使用したときに、追い出される可能性があります。追い出されたアプリに対して、非推奨のCameraAPIではonError()が呼ばれます。Camera2では、onDisconnected()が呼ばれます。

・カメラハードウェアが対応しているデバイスの場合、異なるアプリが異なるカメラを同時に開いてつかうことが出来ます。しかしながら、複数を同時に使用する場合、それぞれのカメラでパフォーマンスの劣化や、機能の制約を受けることが有ります。カメラサービスによりそれらが計測された場合無効化されることが有ります。
この変更により、優先度の低いクライアントアプリは他のアプリが同じカメラに直接接続していない時であっても追い出されることが有ります。

・カメラへのアクセスは現在のデバイスユーザーのユーザープロファイルに限定されます。カレントユーザーを変更した場合、アクティブなカメラは以前のユーザーアカウントから追い出されます。
これは例えば、ゲストアカウントで使用していたカメラサブシステムが別のユーザーアカウントに変更した時に実行プロセスを残すことが出来ないことを意味します。

Runtime

以前のバージョンのDalvikは間違ったアクセスルールでnewInstance()をチェックしていた問題について、ARTランタイムはnewInstance()メソッドに対するアクセスルールを正しく実装します。
アプリでnewInstance()を使用していてアクセスチェックを超えたいときは、引数にtrueをセットしてsetAccessible()を呼んでください。もし、アプリでv7 appcompat libraryv7 recyclerview libraryを使用している場合はそれらのライブラリーを最新版のものに変えてください。あるいは、あらゆるカスタムクラスで、コンストラクターがXMLからアクセス可能なように更新されることを確認してください。

このリリースでは動的リンクが更新されています。動的リンクはライブラリーのsonameと自身のパスの違いを識別できるようになり(public bug 6670)sonameによる検索が実装されました。従来のアプリは良くないDT_NEEDEDエントリー(通常ビルドを行うマシンの絶対パス)を持っていて読み込みに失敗することが有ります。

dlopen(3) RTLD_LOCALフラグが正しく実装されました。RTLD_LOCALはデフォルトなのでdlopen (3)を呼び出してRTLD_LOCALを明示的に使用しないと影響を受けます(アプリがRTLD_GLOBALを明示的に使用していない場合)
RTLD_LOCALシンボルは(DT_NEEDEDエントリーによりリファレンスがつけられるのとは対照的に)後の dlopen (3) を呼び出すことによってライブラリーが呼び出せなくなります。

以前のバージョンのAndroidで、アプリがtext relocationsによる共有ライブラリーの読み込みが必要だった場合、システムは警告を表示しますが、読み込むことは可能です。このリリースで、target SDKバージョンが23以上だった場合はライブラリをシステムが拒否します。
ライブラリーの読み込みが失敗したことを知るために、 dlopen (3) の失敗ログを取る必要があります。dlerror (3) が返す問題点を説明するテキストを含めます。再配置を行うための方法について学ぶにはこの
guideを呼んでください。

APKの正当性確認

プラットフォームはAPKの正当性確認をより厳しく行うようになりました。
ファイルがmanifestで宣言されるがAPK内に存在しなかった場合APKは不正と判別されます。いかなるコンテンツが削除された時も、再度署名を行う必要があります。

USB接続

USBポートに端末が接続された時、標準では充電のみモードとなります。USB接続により端末やそのコンテンツにアクセスするときは各通信についてユーザーが明示的に許可を与える必要があります。もしアプリがUSBポートを経由した端末との通信をサポートしているなら、ユーザーが通信を明示的に許可する必要が有ることに注意してください。

Android for Workの変更点

このリリースではAndroid for Worksについて以下の様な変更があります。
・Personal context中の仕事用連絡先
Googleダイアラーはユーザーが過去の通話履歴を見た時に仕事用の連絡先が表示されるようになりました。
setCrossProfileCallerIdDisabled()にTrueを渡すことでGoogleダイアラーの通話履歴に仕事用の連絡先が表示されなくなります。setBluetoothContactSharingDisabled()にfalseを設定している時のみ、業務用の連絡先が個人用連絡先とともにBluetooth経由のデバイスに表示されます。デフォルトではtrueが設定されています。

・Wi-Fi設定の削除 仕事用プロファイルが削除された時にプロファイルオーナーがaddNetwork()を呼ぶなどして追加したWi-Fi設定が削除されます。

・Wi-Fi設定のロックダウン アクティブデバイスオーナーによって作られたあらゆるWi-Fi設定はWIFI_DEVICE_OWNER_CONFIGS_LOCKDOWNにゼロ以外を設定することで、ユーザーにより修正したり削除することが出来なくなります。ユーザーは自分自身のWi-Fi設定を作ったり編集することは出来ます。アクティブデバイスオーナーは自分以外の人が作ったものを含めて全てのWi-Fi設定を修正したり削除する特権を持っています。

・Googleアカウントを加える 時にWork Policy Controllerをダウンロードする
Googleアカウントを管理されたContextのデバイス外から追加されたWork Policy Controller(WPC)アプリによって観理する必要がある時、アカウント追加フローでユーザーに適切なWPCをインストールすることを促します。
この挙動は、アカウント設定からアカウントを追加するときも、デバイスの初期セットアップウィザードにも当てはまります。

・特定のDevicePolicyManagerAPIの挙動が変更されます。
setCameraDisabled()メソッドを呼んでも、呼び出したユーザーのカメラだけに影響します。
管理されたプロファイルからそれを呼んでもプライマリーユーザー上で動いているカメラアプリには影響しません。

setKeyguardDisabledFeatures()メソッドはデバイスオーナーに加えてプロフィールオーナーでも使用可能になりました。

→プロファイルオーナーは以下のキーガード規制を設定できます。
KEYGUARD_DISABLE_TRUST_AGENTSKEYGUARD_DISABLE_FINGERPRINT(プロファイルの親ユーザーがもつKeyguard設定に影響します)

KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS管理者プロファイルのアプリに寄って作成されたNotificationのみに影響します。

createAndInitializeUser()createUser()メソッドは非推奨になりました。

setScreenCaptureDisabled()メソッドは指定したユーザーのアプリが全面にある時、assist structureもブロックします。

EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUMは標準でSHA-256となります。SHA-1は後方互換性のために継続してサポートされますが、将来的に削除される予定です。
EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUMはSHA-256のみを受け入れます。

→Android 6.0(API level23)に存在していたデバイス初期化APIは削除されました。

→EXTRA_PROVISIONING_RESET_PROTECTION_PARAMETERSが削除されました。そのため、NFC bumpプロビジョニングはプログラムでデバイスのファクトリーリセットプロテクトを解除することが出来なくなります。

→管理されたデバイスのNFCプロビジョニングによって、デバイスオーナーアプリにデータを送るためにEXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE extraが使用可能になりました。

→Work Profileやassist layer等no
Android for Work API群は M runtime permissionに最適化されました。
新しいDevicePolicyManager permission API群はM以前のアプリには影響しません。

ACTION_PROVISION_MANAGED_PROFILEあるいはACTION_PROVISION_MANAGED_DEVICE intentを通じて開始されたセットアップフローの同期処理がユーザーによってキャンセルされた場合、システムは戻り値RESULT_CANCELEDを返すようになりました。

・その他API群の変化
→Data Usage:android.app.usage.NetworkUsageStatsクラスはNetworkStatsにリネームされました。

・グローバル設定の変更
→以下の設定はsetGlobalSettings()によって設定できなくなります。
・BLUETOOTH_ON
・DEVELOPMENT_SETTINGS_ENABLED
・MODE_RINGER
・NETWORK_PREFERENCE
・WIFI_ON

→以下の設定がsetGlobalSettings()によって設定できるようになります。

WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN

Except as noted, this content is licensed under Creative Commons Attribution 2.5. For details and restrictions, see the Content License.

Android6.0の新機能API

$
0
0
Android 6.0(M)はアプリ開発者とユーザーに新機能を提供します。
このドキュメントでは注目のAPI群について紹介します。

原文

Android6.0 API群

開発を始める

Android6.0向けのアプリ開発を始めるためにはまずはじめにget the Android SDKでAndroid SDKを取得します。次にSDK Managerを使ってAndroid 6.0 SDK PlatformとSystem Imagesをダウンロードします。

target API levelをアップデートする

更新されたアプリを公開する前にテストを行ってください。
アプリを端末に最適化するために、targetSdkVersionに23を設定し、Android system imageにインストールしてテストを行います。

minSdkVersionで指定したバージョンではサポートされていないAPIを使用する場合には、使用する前にシステムのAPI levelを確認することで、古いバージョンをサポートしたまま新しいAPIを使用することが出来ます。
後方互換性を維持する方法について詳しくはSupporting Different Platform Versionsを読んでください。

API levelの動作について詳細はWhat is API Level?を読んでください。

指紋認証

このリリースでは新しいAPIによって対応端末上で指紋を使ったユーザー認証を行わせることが可能です。このAPIはAndroid Keystore systemと連携します。

指紋を使ったユーザー認証を行わせるためには、新しいFingerprintManagerクラスを使ってauthenticate()メソッドを呼びます。アプリが指紋認証に対応した端末上で動作している必要があります。
指紋認証を行うためのUIを実装するときには標準のAndroid指紋アイコンをUI上で使用する必要があります。Andriod指紋認証アイコン(c_fp_40px.png)はFingerprint Dialog sampleに入っています。もし、指紋認証を使う複数のアプリを作っている場合、それぞれのアプリで独立して指紋認証を行う必要が有ることに注意してください。

アプリ内でこの機能を使用するときまずmanifestにUSE_FINGERPRINT permissionを追加します。

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

指紋認証の実装を見るにはFingerprint Dialog sampleを参照してください。 指紋認証を他のAndroid APIと連携させるデモについてはFingerprint and Payment APIsのビデオを見てください。

この機能を試すには以下の手順に従ってください。
1. Android SDK Tools Revision24.3をインストールします。

2.エミュレーターに指紋を登録するために、設定>セキュリティ>指紋 を選び、導入画面従って「登録の開始」まで画面を進めてください。

3.エミュレータで指紋のタッチイベントをエミュレートするために、次のコマンドを使います。同じコマンドをロックスクリーンやあなたのアプリでも使用してください。

adb -e emu finger touch <finger_id>

Windowsを使用している場合は、finget touch に続いて telnet 127.0.0.1 を実行してください。

証明の確認

最後に端末をロック解除してからどれくらいの時間が経っているかに基づいてアプリ内でユーザーを認証することが出来ます。この機能により、ユーザーはアプリ専用のパスワードを覚えておく必要がなくなり、開発者は認証ユーザーインターフェイスを実装する必要もなくなります。アプリはユーザー認証のために公開鍵または秘密鍵を合わせてこの機能を使用する必要があります。

ユーザーが認証に成功したら、同じ鍵を再利用できるタイムアウト期間を設定するために、KeyGeneratorあるいはKeyPairGeneratorをセットアップするときにsetUserAuthenticationValidityDurationSeconds()を呼びます。

過剰に再認証ダイアログが表示されることを防ぐために、アプリはcryptographicオブジェクトを最初に使って見る必要があります。もし、タイムアウトになったらcreateConfirmDeviceCredentialIntent()メソッドを呼ぶことでアプリ内で再認証を行うことが出来ます。

この機能を実装する方法についてはConfirm Credential sampleを見てください。

App Linking

このバージョンではより強力なapp linkingが提供されAndroidのintentの仕組みが強化されました。この機能によりアプリとあなたの所有するWebドメインを連携させることが出来ます。この連携に基づき、プラットフォームは特定のURLに関連するデフォルトアプリを認識しユーザーがアプリを選択するプロンプトをスキップする事ができます。この機能を実装する方法はHandling App Linksを参照してください。

Auto Backup for Apps

システムはアプリの全データを自動的にバックアップ・復元するようになりました。アプリのtarget APIがAndroid6.0(API Level23)以上の時、この機能が有効になります。コードには何一つ追加する必要はありません。もしユーザーがGoogleアカウントを削除するとこれらのバックアップデータも削除されます。この機能がどのように働き、ファイルシステム上でバックアップがどのように構成されるかについてはConfiguring Auto Backup for Appsを読んでください。

Direct Share

このリリースではユーザーが直感的に素早く共有を行うことが出来るAPIが導入され、あなたのアプリでDirect share対象となる特定のActivityを指定することが出来るようになりました。Direct shareの対象は共有メニューを通じてユーザーに表示されます。この機能によりユーザは連絡先やその他アプリに登録された対象にコンテンツを共有することが出来ます。Direct shareの対象は例えば別のソーシャルネットワークアプリのActivityなどです。これによりユーザーはアプリ内に登録された特定の友人やコミュニティにコンテンツを直接共有できます。

Direct share対象を有効にするためにはChooserTargetServiceを継承したクラスを作成します。サービスをManifestで宣言します。BIND_CHOOSER_TARGET_SERVICEpermissionを宣言し、SERVICE_INTERFACEアクションのintent filterを使用します。

ChooserTargetServiceをどのように宣言するかの例

<service android:name=".ChooserTargetService"
android:label="@string/service_name"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>

ChooserTargetServiceで表示する各Activityに対してmanifestで meta-data 要素に name "android.service.chooser.chooser_target_service"を追加します。

<activity android:name=".MyShareActivity”
android:label="@string/share_activity_label">
<intent-filter>
<action android:name="android.intent.action.SEND" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value=".ChooserTargetService" />
</activity>

声による対話

このリリースでは新しい音声対話APIがVoice Actionsとして提供されます。これにより、声での対話ができるアプリを開発することが出来ます。音声アクションによりActivityを起動したかどうか調べるにはisVoiceInteraction()メソッドを呼びます。もし、そうであればVoiceInteractorクラスを使って、ユーザーに選択リストから選ばせるなどの音声操作をリクエストできます。

多くの音声対話は、ユーザーの音声アクションから開始されます。しかし、ユーザーによる入力がない状態で音声対話のActivityを始めることも出来ます。例えば、他のアプリから音声対話によって起動された時、音声対話のActivityを起動するIntentを送信することが出来ます。
あなたのActivityがユーザーからの音声対話で起動されたのか、他の音声対話アプリから起動されたのかを調べるにはisVoiceInteractionRoot()メソッドを使います。もし他のアプリからActivityが起動されていたらメソッドはfalseを返します。これによりあなたのアプリはIntentのアクションをユーザーに確認する事もできます。

音声アクションを実装する方法についてはVoice Actions developer site

音声アクションを実装する方法について学ぶにはVoice Actions developer siteを読んでください。

Assist API

このリリースではアシスタントを通じて、ユーザーとアプリを結びつける新しい方法を提供します。
この機能を使用するにはユーザーは現在の状況を使用できるようにアシスタントを有効にする必要があります。
有効にすれば、ユーザーはホームボタンを長押しすることでどんなアプリからでもアシスタントを呼び出すことが出来ます。

アプリで現在状況をアシスタントと共有したくない時は、FLAG_SECUREフラグを設定します。
新しいAssistContentクラスを使うことでプラットフォームがアシスタントに共有する標準的な情報のセットに加えて、アプリから追加の情報をアシスタントと共有することが出来ます。

アプリ内の追加の状況をアシスタントに提供するには以下の手順を行います。

1. Application.OnProvideAssistDataListenerインターフェースを実装する。
2.registerOnProvideAssistDataListener()を使用してlistenerを設定する。
3.Activity特有の状況についての情報を提供するために、onProvideAssistData()コールバックをOverrideして、必要ならば新しいonProvideAssistContent()コールバックをOverrideします。

ストレージデバイスを取り込む

このリリースではユーザーがSDカードのような拡張ストレージデバイスを取り込むことが出来ます。拡張デバイスを取り込むには内部ストレージのように振る舞わせるために初期化して暗号化します。この機能によりユーザーはアプリやアプリの個人的なデータをストレージデバイス間で移動することが出来ます。アプリを移動出来るかどうかについて、システムはmanifestにあるandroid:installLocationの設定に従います。

もし、アプリが以下のAPI群やフィールドにアクセスしているならば、それらのファイルパスは内部ストレージと外部ストレージを移動した時に動的に変化することに注意してください。ファイルパスをビルドするときには、常にこれらのAPIを呼ぶことを強く推奨します。
ハードコードしたファイルパスを使ってはいけません。また、1度作った絶対パスがずっと使えるとは限りません。

Contextのメソッド
getFilesDir()
getCacheDir()
getCodeCacheDir()
getDatabasePath()
getDir()
getNoBackupFilesDir()
getFileStreamPath()
getPackageCodePath()
getPackageResourcePath()

ApplicationInfoのフィールド
dataDir
sourceDir
nativeLibraryDir
publicSourceDir
splitSourceDirs
splitPublicSourceDirs
次のコマンドでデバッグのためにUSB On-The-Go (OTG)で接続されているUSBドライブに対してこの機能を有効にすることができます。

$ adb shell sm set-force-adoptable true


Notifications

このリリースではNotificationに対して次のAPIに変更が加わっています。
・新しいINTERRUPTION_FILTER_ALARMSフィルターレベルは「通知を非表示」が「アラームのみ」に設定されている時に合致します。
・新しいCATEGORY_REMINDERカテゴリーはユーザースケジュールのリマインダーをそれ以外の(CATEGORY_EVENT)やアラーム(CATEGORY_ALARM)と区別します。
・新しいIconクラスはsetSmallIcon()setLargeIcon()メソッドによりNotificationに添付することが出来ます。同様にaddAction()メソッドはdrwableリソースIDの代わりにIconオブジェクトを使うことが出来ます。
・新しいgetActiveNotifications()はアプリのNotificationのうち現在残っているものを探すことが出来ます。この機能を実装する方法についてはActive Notifications sampleを読んでください。

Bluetoothスタイラスのサポート

このリリースではBluetoothスタイラスを使ったユーザー入力のサポートが強化されました。ユーザーは互換性のあるBluetoothスタイラスを電話やタブレットとペアリング出来ます。接続されると、タッチスクリーン上の位置情報に加えてスタイラスから筆圧とボタン情報を受け取ります。これにより、タッチスクリーン単独より幅広い表現が可能となります。

View.OnContextClickListenerGestureDetector.OnContextClickListenerオブジェクトをActivity内で登録することでアプリでスタイラスのボタンが押されたことを検知したり第二ボタンの処理を実装できます。

スタイラスボタンを受け付けるためにMotionEventメソッドと定数を使用してください。

・ユーザーがアプリのボタンをスタイラスでタッチした場合、getTooltype()メソッドはTOOL_TYPE_STYLUSを返します。

・TargetがAndroid 6.0 (API level 23)のアプリでは、スタイラスの主ボタンをユーザーが押した時getButtonState()メソッドがBUTTON_STYLUS_PRIMARYを返します。スタイラスに副ボタンが有り、ユーザーが押した時はBUTTON_STYLUS_SECONDARYを返します。
ユーザーが主ボタンと副ボタンを同時に押した時は、BUTTON_STYLUS_PRIMARYBUTTON_STYLUS_SECONDARYを合算して返します。
・古いプラットフォームをターゲットとしている場合はgetButtonState()メソッドは、主ボタンをおした時、BUTTON_SECONDARYを返し副ボタンをおした時、BUTTON_TERTIARYを返します。

改良されたBluetooth Low Energyのスキャニング

もし、アプリでBluetooth Low Energyのスキャンを行っているなら、setCallbackType()メソッドでBluetooth Low Energyデバイスが見つかった時のコールバックをシステムに要求できます。
システムは長時間ScanFilterにセットした内容と一致するadvertisement packetを探します。この方法は以前のプラットフォームに比べてより効率がいいです。

Hotspot 2.0 リリース1対応

このリリースによりNexus6とNexus9上でHotspot 2.0 リリース1のサポートを追加します。
アプリ内でHotspot2.0の認証を準備するために、新しいWifiEnterpriseConfigクラスのsetPlmn()setRealm()メソッドを使用します。WifiConfigurationオブジェクト内で、FQDNproviderFriendlyNameフィールドを設定できます。isPasspointNetwork()メソッドはHotspot 2.0アクセスポイントを見つけます。

4K Display mode

アプリは4K解像度に対応している端末で4K解像度へアップグレードするよう要求できるようになりました。
現在の物理的な解像度を知るためには新しいDisplay.ModeAPIを使用します。

もしUIが低い論理的解像度で描画されていて、高解像度な物理解像度にアップスケールされている時、物理解像度を返す
getPhysicalWidth()は論理解像度を返すgetSize()の値と差がでることに注意してください。

Android 6.0 APIs | Android Developersを設定することで、アプリが実行されている時に物理解像度を変更するようシステムに要求することが出来ます。この機能は4Kディスプレイ解像度に切り替えたい時に便利です。
4Kディスプレイモードの時、UIはオリジナルの1080pで描画され4Kにアップスケールされますが、SurfaceViewオブジェクトは元の解像度を維持することが有ります。

テーマに対応したColorStateLists

Theme atrributeはAndroid6.0(API level 23)が動作する端末においてColorStateListをサポートします。Resources#getColorStateList()とResources#getColor()は非推奨となりました。もしこれらのAPIを呼んでいたらなら代わりにContext#getColorStateList()やContext#Android 6.0 APIs | Android Developersメソッドを代わりに呼んでください。このメソッドはContextCompatとしてv4 appcompatライブラリでも使用可能です。

音声機能

このリリースでは音声処理の機能が改善されています。
・新しいandroid.media.midiAPI群によりMIDIプロトコルがサポートされます。このAPIを使用することでMIDIイベントを送受信できます。
・新しいAudioRecord.BuilderAudioTrack.Builderクラスによってそれぞれデジタルオーディオの録音と再生オブジェクトを作って、オーディオソースを指定しシステム標準の同期プロパティーを上書きできます。
・APIはオーディオ関連した入力デバイスをフックします。これは特にアプリで、ゲームコントローラーやリモコンを使って接続したAndroid TVで音声検索を開始したい時便利です。システムはユーザーが検索を開始すると、新しいonSearchRequested()コールバックを実行します。コールバックからInputDeviceオブジェクトを取得し、新しいhasMicrophone()メソッドを呼ぶことで入力デバイスがマイクを内蔵しているか確認します。
・新しいgetDevices()メソッドにより現在システムに接続されている全てのオーディオデバイスのlistを取得します。オーディオデバイスが接続されたり取り外された時にシステムから通知を受けるためにAudioDeviceCallbackを登録することもできます。

ビデオ機能

このリリースではビデオ処理のAPIについて以下の心機能を含みます。
・新しいMediaSyncは音声出力とビデオストリームを同期しやすくします。オーディオバッファーはコールバックによりお互いブロックされることなく作成・取得できます。それは可変ビットレートもサポートします。
・新しいEVENT_SESSION_RECLAIMEDイベントにより、アプリが開いたセッションがリソースマネージャーに再取得されたことを検知できます。アプリがDRMセッションを使用しているならば、このイベントを検出してリソースマネージャーに再取得されたセッションを使用しないようにしなくてはいけません。
・新しいERROR_RECLAIMEDエラーコードによりコーデックが使用しているメディアセッションをリソースマネージャーが再取得したことがわかります。この例外が発生したら、コーデックを開放して終了状態に移行する必要があります。
・新しいgetMaxSupportedInstances()インターフェースで現在のコーデックインスタンスがサポートする最大数を取得します。
・新しいsetPlaybackParams()メソッドでメディア再生のレートや早送り、スローモーション、などを設定できます。ビデオと関連したオーディオ再生をスローにしたりスピードアップすることも出来ます。

カメラ機能

このリリースには以下のようにカメラのフラッシュやカメラの画像現像処理にアクセスするための新しいAPIが含まれています。

フラッシュライトAPI

カメラデバイスがフラッシュを持っているなら、setTorchMode()メソッドを呼ぶことでフラッシュのトーチモードを切り替えてフラッシュの点灯・消灯をカメラデバイスを開くこと無く行うことが出来ます。アプリはフラッシュ及びカメラに対して排他的な所有権を持ちません。トーチモードはカメラデバイスが利用不可能になった時や、その他フラッシュを灯すために必要なカメラのリソースを保持できなくなった時にオフになり利用できなくなります。他のアプリがsetTorchMode()を呼んだ時もトーチモードはオフになります。最後のアプリがトーチモードを終了するとトーチモードはオフになります。

トーチモードの状態について通知を受けるためにregisterTorchCallback()メソッドを呼んでコールバックを登録できます。コールバックが登録されたら最初にすぐに現在のフラッシュライトを持っている全てのカメラのトーチモードの状態が渡されます。

トーチモードがオン、オフされるとonTorchModeChanged()が呼ばれます。

再処理API

Camera2APIはYUV対応を拡張して、Opaque formatの画像再処理を提供します。画像処理が可能であるか確認するためにgetCameraCharacteristics()を呼んでREPROCESS_MAX_CAPTURE_STALLキーを確認します。端末が再処理に対応しているならば、createReprocessableCaptureSession()を呼んで再処理可能なキャプチャーセッションを作成して、再処理用の入力バッファ作成を要求できます。

入力バッファーフローをカメラの再処理入力に接続するために、ImageWriterクラスを使用します。中身が無いバッファーを取得するために、以下の手順を行います。
1.dequeueInputImage()メソッドを呼ぶ
2.入力バッファー内にデータをセットする。
3.queueInputImage()メソッドを呼びバッファをカメラに送る。

ImageWriterオブジェクトとPRIVATE画像を使用している時は、アプリが画像データに直接アクセスすることは出来ません。代わりに、queueInputImage()メソッドを呼ぶことで一切バッファーにコピーされること無くImageWriterに渡されます。

ImageReaderクラスはPRIVATEフォーマットの画像ストリームをサポートするようになりました。このサポートによりImageReaderが出力する画像のcircular image queueをアプリで管理できるようになりました。現像のために1つ以上の画像を選択してImageWriterに送信します。

Android for Work機能

このリリースでは次のAndroid for Workの新しいAPIが含まれています。

改良されたCorporate-Ownerd, Single-Use端末の管理

端末の所有者は会社が所有し短期的に貸し出すCorporate-Owned, Single-Use (COSU)端末の管理を改善するために以下の設定を操作できます。
setKeyguardDisabled()メソッドでキーガードを無効、最有効化できます。
setStatusBarDisabled()メソッドでステータスバー(クイック設定やNotification,ナビゲーションを上方向にスワイプしてのGoogle Nowの起動を含む)を無効・最有効化できます。
UserManagerと定数DISALLOW_SAFE_BOOTでsafe bootを無効・最有効化できます。
STAY_ON_WHILE_PLUGGED_IN定数で充電中にスリープになるのを禁止できます。

デバイスオーナーによるアプリのサイレントインストール/アンインストール

デバイスオーナーはPackageInstaller API群を使うことでアプリのサイレント・インストール・アンインストールが可能になりました。
ユーザーの対話を必要とせずデバイスオーナーがアプリを取得しインストールすることで端末を準備することが出来るようになりました。この機能はキオスクなどの端末で各デバイスのGoogleアカウントを有効にする必要なくワンタッチでデバイスを準備するのに便利です。


企業向けのサイレント 証明書アクセス

choosePrivateKeyAlias()をアプリが呼ぶとユーザーが証明書をえらぶプロンプトが開始されます。
プロファイルオーナーやデバイスオーナーはonChoosePrivateKeyAlias()メソッドを呼んで、証明書を要求しているアプリにサイレントでエイリアスを提供できるようになりました。この機能により管理されたアプリをユーザーの対話無しで証明書にアクセスさせることができます。

システムアップデートの自動承認

setSystemUpdatePolicy()でシステムのアップデートポリシーを設定できます。デバイスオーナーは例えばキオスク端末のように自動でアップデートを行ったり、ユーザーによって最大30日間アップデートを遅らせることが出来ます。さらには、管理者は、更新を行う時刻をキオスク端末が使用されていない時間などに設定できます。システムのアップデートが利用可能になるとシステムはワークコントローラーアプリがシステムアップデートポリシーを持っているか確認し、適切に振る舞います。

証明書のインストールを移譲する

プロファイルオーナーやデバイスオーナーはサードパーティーのアプリがDevicePolicyManagerにある次の証明書管理API群を呼ぶことを許可できます。
getInstalledCaCerts()
hasCaCertInstalled()
installCaCert()
uninstallCaCert()
uninstallAllUserCaCerts()
installKeyPair()

データ使用の追跡

プロファイルオーナーとデバイスオーナーはNetworkStatsManagerメソッドにより設定>データ使用量で見ることが出来るデータ統計を問い合わせることが出来るようになりました。
デバイスオーナーが管理するメインユーザーの使用データーへのアクセス権を得ている間、プロファイルのオーナーは管理するプロファイルに関するデータを問い合わせる許可が自動的に与えられます。

Runtime permissionの管理

プロファイルオーナーとデバイスオーナーは全てのランタイムリクエストに対してユーザーにパーミッションを設定させるか、自動的に承認/拒否させるかなどPermissionのポリシーをsetPermissionPolicy()を使って設定できます。後者の場合、プロファイルオーナーやデバイスオーナーが設定内にあるアプリの権限で作成したPermissionについてユーザーは変更出来なくなります。

設定内のVPN

VPNアプリを設定>もっと見る>VPN内でVPNアプリを見つけることが出来ます。VPNを使用するときのNotificationはVPNが設定される方法を特定します。
プロファイルオーナーにNotificationがVPNで管理されたプロファイルか、個人プロファイルか、その両方なのかを示します。

Work statusのNotification

managed profileによって起動されたアプリのActivityが最前面にあるときは常にステータスバーにブリーフケースアイコンが表示されるようになりました。加えて管理されたプロファイル内でアプリのActivityをデバイスがロック解除した時、ユーザーがワークプロファイルに入ったことがToastで表示されます。

Except as noted, this content is licensed under Creative Commons Attribution 2.5. For details and restrictions, see the Content License.

ここが凄いよ AndroidStudioとGradle

$
0
0
Androidの開発環境はここ2年でものすごく変わり始めていますが、
もっともインパクトが大きい変化はEclipseからAndroid Studioへの移行でしょう。
Eclipseの開発環境は今後サポートされなくなることが告知されているので早めにAndroid Studioへ移行する必要があります。

Android StudioといえばGradleへの変化がもっとも大きなハイライトです。
Gradleがすごいよ!という話はよく聞きますが実際にどういう時に凄いかという話はあまりされていない気がします。
ということで、実際に使ってみて特に便利と感じたところを紹介します。

Gradleの凄いところ

ライブラリーの取り込みが凄い


Gradleを使って一番最初に感じるメリットがライブラリーの取り込みです。
Eclipseでライブラリーの取り込みってめんどくさいですよね
ライブラリーをダウンロードしてJarを取り込まないといけなかったりプロジェクトを読み込んでisLibraryを設定したり・・・
ライブラリーのバージョンが上がるたびに同じことの繰り返しだし、複数マシンで開発しているとマシンごとにライブラリーのバージョンが違ったりすることも

では、Gradleだとどうするかというと
build.gradleの

dependencies {

}

で囲まれた中に使用したいライブラリをcompile につづいて指定するだけ

例えばGooglePlay Servicesを取り込みたければ

compile 'com.google.android.gms:play-services:7.8.0'

と一行入れるだけであとは自動でライブラリーをダウンロードしてインポートまで完了します。
もちろん別のパソコンで開発するときもbuild.gradleを共有すれば自動的にダウンロードしてくれるのでライブラリーを共有に加えたり環境構築に手間をかける必要はありません。
バージョン番号も明記しているので間違って別のバージョンが組み込まれていたり古いバージョンが残ってしまうこともありません。

ライブラリーのバージョンについて開発中はとりあえず最新を使いたいという時もあると思います。
そういうときは+を使います。
例えばとにかく最新を使いたければ

compile 'com.google.android.gms:play-services:+'

というようにします。
これでGradleBuildが実行されるたびに最新をチェックして新しいものが有れば自動的にダウンロードしてインストールしてくれます。

この+は柔軟な指定ができて、例えばバージョン7とバージョン8で互換性がないのでバージョン7の最新版を使いたいという場合は次のように指定します。

compile 'com.google.android.gms:play-services:7.+'

このように固定値と最新を合わせて指定することができます。

2.BuildTypeとFlavorが凄い

例えば開発版では開発用のサーバーに接続していて内部のデータが異なる場合など、アプリを開発していると、開発版と正式版を同時にインストールしたいということがよく有ります。
正式版をアンインストールして開発版をインストールするとデータも全て消えるので面倒です。
AndroidではApplicationIDが同じアプリを複数インストール出来ないため、Eclipseではプロジェクトをコピペしてパッケージ名を置き換えるみたいなリスキーかつ大規模な改造をするとか処理部分をライブラリーにしてそれを読むだけのアプリを作るとか、どちらにしてもかなり面倒な作業が必要です。

Gradleを使うとデバッグ版とリリース版でパッケージを変えるなんてことが簡単にできます。

例えばデバッグで書きだした時はパッケージ名に.debugをつけるようにするにはModule内にあるbuild.gradleの

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

となっているところに、デバッグの時だけ末尾に.debugを追加するよう

debug{
applicationIdSuffix ".debug"
}

を追加して次のようにします。

buildTypes {
debug{
applicationIdSuffix ".debug"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

これで、デバッグビルドの時とリリースビルドの時でapplicationIdを変えることが出来ます。
applicationIdが変わることでAndroidは別のアプリとして認識するので同時に製品版とデバッグ版をインストールできて、保存されるデータも別々に持つことが出来ます。

デバッグとリリースで別のサーバーに接続するには、
左上のProjectを押して、表示タイプをProjectにします。(これによりファイルシステム上のパスと表示が一致します)


srcフォルダ直下にある
main/res/values/strings.xmlを開きurlを追加します。
ここには本番用のURLをセットします。
/src/main/res/values/strings.xml

<resources>
<string name="app_name">Sample application</string>
<string name="url">http://firespeed.org</string> <!-- 追加 -->
</resources>

次に、srcフォルダ直下にdebug というフォルダを作りmain配下同様に、res/values/というフォルダを作ったらその中にstrings.xmlを追加します。
strings.xmlの書き方は普通のstrings.xmlと同様ですが、debug時だけ書き換えたい内容を入れるだけです。
/src/debug/res/values/strings.xml

<resources>
<string name="url">http://firespeed.org/debug</string> <!--テスト用サーバー -->
</resources>

このようにすることで、本番用サーバーとテスト用サーバーを書き換えることが出来ます。
URLを取得するときは、普通にxmlからStringを取得する時同様、Context#getString(int resId)を呼びます。

getString(R.string.url);

デバッグでビルドするか、リリースでビルドするかに応じて自動的にgetString()で取得する内容が変わります。
debugに書かれていないものについては自動的にmainから取得するので、書き換えたい項目だけを記載すれば大丈夫です。
この方式ではstrings.xmlだけでなくres内のファイルはどれでも好きな様に書き換えることが出来ます。
例えばrelease時だけ色を変えるとか、レイアウトを変えるとか、アイコンを変更するようなことも出来ます。

Javaの処理を書き換えたい時もあると思います。
Javaではmainとdebugを自動的にマージしてくれません。
mainとdebugに同じクラスがあるとエラーとなるので、debug同様にreleaseフォルダを作って対応します。
src/debug/java/com/example/sample/Hoge.java

public class Hoge{
// debugで使う処理
}


src/release/java/com/example/sample/Hoge.java

public class Hoge{
// releaseで使う処理
}


ファイルを丸ごと切り替えるのが大げさな場合、BuildConfig.BUILD_TYPEを使って書き換えることも出来ます。
例えばデバッグ時とリリース時でstaticな定数を書き換えるには次のようにします。

public class Hoge{
public final static String SERVER_PATH;
static {
if (BuildConfig.BUILD_TYPE.equals("debug")) {
SERVER_PATH = "http://firespeed.org/debug/"; // 開発サーバーのURL
} else {
SERVER_PATH = "http://firespeed.org/"; // 本番サーバーのURL
}
}
// 中略
}


Flavorで似たような別のアプリを作る

接続先は本番環境とテスト環境を切り替えたいけれどどちらもデバッグを使いたいというようなことも有ると思います。
そのような場合はFlavorを使うと便利です。
Flavor(味付け)という名前が示すように、本質的な部分は同じながら、一部処理を変えて別アプリを作りたいという場合などに使用します。
上記の例の他に、製品版と体験版、お客さんごとのカスタマイズ、マーケットごとの仕様調整などにも便利です。

Flavorを宣言するにはBuild.gradleのAndroid内で次のように設定します。

android{
//中略
productFlavors{
flavor1{
// flavor 1 特有の内容
}
flavor2{
// flavor 2 特有の内容
}
}
}

例えばflavor1ではアプリケーションIDをorg.firespeed.foo、ターゲットSDKバージョンを22
flavor2ではアプリケーションIDをorg.firespeed.bar、ターゲットSDKバージョンを23としたい場合は次のようにします。

android{
//中略
productFlavors{
flavor1{
// flavor 1 特有の内容
applicationId "org.firespeed.foo"
targetSdkVersion 22
}
flavor2{
// flavor 2 特有の内容
applicationId "org.firespeed.bar"
targetSdkVersion 23
}
}
}

あとの処理はBuildTypeと同様にmainと同一階層にFlavorの名前でフォルダ名を作成することで特定のFlavorだけファイルを入れ替えることが出来ます。
/src/main/res/values/strings.xml

<resources>
<string name="app_name">Sample application</string>
<string name="url">http://firespeed.org</string></resources>


/src/flavor2/res/values/strings.xml

<resources>
<string name="url">http://firespeed.org/flavor2</string> <!--Flavor2だけが使うURL -->
</resources>


Java内で処理を切り替えるときは、BuildConfig.BUILD_TYPEの代わりにBuildConfig.FLAVORを使います

public class Hoge{
public final static String SERVER_PATH;
static {
if (BuildConfig.FLAVOR.equals("flavor2")) {
SERVER_PATH = "http://firespeed.org/flavor2/"; // Flavor2だけが使うURL
} else {
SERVER_PATH = "http://firespeed.org/";
}
}
// 中略
}

起動するFlavorを変更するにはBuild Variantsを選択します。

flavorを切り替えることでどのFLAVORをデバッグ/リリースバージョンとして実行するかを決めることが出来ます。

出力するapk名を変える

複数のアプリを開発していると、どれがどのAPKかわからなくなったりします。
また、日付やバージョン番号を知りたいということもあります。
そのような場合にもbuild.gradleで設定できます。

android{
}

の中に

android.applicationVariants.all { variant ->
variant.outputs.each { output ->

}
}

を指定してファイル名を設定します。
記載方法がAndroidで使うJavaとはちょっと違って見えるかもしれませんが、要するに 引数として渡されたoutputに対して処理を行っていく形になります。
output.outputFileに出力するFileクラスのインスタンスを渡すことで、どのようにファイルを出力するかを決めることが出来ます。

たとえば<FLAVOR名>_<バージョンコード>_<ビルド日時>.apkとしたければ次のように指定します。

apply plugin: 'com.android.application'
android {
// 中略
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def flavor = variant.mergedFlavor
def file = output.outputFile
def flavorName = variant.flavorName
def versionCode = flavor.versionCode
def date = new java.text.SimpleDateFormat("yyyy-MM-dd HH.mm").format(new Date())
def fileName = "${flavorName}_${versionCode}_${date}.apk"
output.outputFile = new File(file.parent, fileName)
}
}
// 中略
}



他にもReleaseのビルドに成功したらgitにコミットするとか出来ること盛りだくさんという感じです。

Android Studioのここが凄い

Android Studioは特殊な名前になっていますが、専用設計ではなく、ベースとなるのはIntelliJ IDEAというIDEです。
なので、Android StudioがすごいというかIntteliJが凄いです。
Googleにこれまでの資産をなげうってまで移行する決心をさせたほど凄いです。
凄いことを言い始めるときりがないのですが、使っていて感じた凄いところを幾つか紹介します。

サジェストが凄い

IDEを使う最大のメリットとも言えるのがサジェスト、Android Studioはこれも良く出来ています。
例えば HogeFugaFooBarクラスがあった場合、EclipseではHogeのように前方一致で打った時だけサジェストがされていました。
それがAndroid StudioではFugaとかFooとかでもサジェストしてくれるようになりました。
これのおかげで、例えばActivityを呼び出したいけれど、Activityのファイル名を忘れた というような場合にActivityとだけ打てばMainActivityやSubActivityなどActivityの一覧が表示されるようになりました。
さらにはMainActivityの場合、MAのように頭文字だけでもサジェストしてくれますし、MaiActのように頭文字+数文字の組み合わせみたいなものでもサジェストしてくれます。

コード追跡がえらい

EclipseではF3キーを押すと、それが宣言されていたり、作成されている場所へ飛ぶ機能があります。
概ね問題なく動くのですが、問題はRクラスの中身に飛ぶ時。
RクラスはAndroidがリソースファイルを参照するために作るStaticなintの集合体を持っていて、レイアウトファイルを参照したりするのに使う

findViewById(R.id.hoge);

のR.id.hogeは、XMLを元に自動生成されたRクラスの中にあるstatic inner classであるidクラスにあるstaticでfinalなint型の数字hogeを意味します。
そのため、EclipseでF3キーを使ってR.id.hogeの宣言場所を探すと、Rクラスが開いていました。
しかし、Rクラスにあるのはintの羅列ばかりで実際にそれが役立つことは殆どありません。IDEとしての動きは正しいけれど使えない状態です。

Android Studioでは同様の機能をCommand(WindowsはCtrl)+Bキーで行います。
そして、この時、R.id.hogeはhogeを指定しているレイアウトファイルのXMLに飛びます。 便利過ぎる。

他にも変数から、宣言している場所に飛ぶと変数を宣言している場所に飛びますが、そうではなく変数の型を作成しているところに飛びたいということも良く有ります。
そのような場合はCommand(WindowsはCtrl)+Shift+Bキーで飛べます。

コードを追跡していたらInterfaceにぶつかって、実装が見られないなんてこと、よくあると思うのですが、その時はCommand(WindowsはCtrl)+Alt+Bキーでそのインターフェイスを実装しているクラス一覧やメソッド一覧が表示され飛ぶことが出来ます。

継承元のクラスに飛びたいときはCommand(WindowsはCtrl)+Uです。

Projectが便利

Android Studioに移行して最初に戸惑うのがProjectの概念かと思います。
Android StudioはProjectの考え方がEclipseと根本的に異なります。
EclipseでいうProjectはAndroid StudioではModuleと呼ばれます。 これはほぼイコールで、ビルドする単位つまりアプリやプロジェクト一つ一つがそれぞれ別のModuleとなります。

一方でEclipseでいうWorkspaceに当たるのがAndroidStudioではProjectになります。これは概念としては近いのですがノットイコールです。(Workspace的に使ってもいいけれど)
Eclipseは一度Workspaceを作ると切替が面倒だったので、よく使うProjectは全てWorkspaceに入れておくという人も多かったと思います。そのため、一つのWorkspaceには複数のProjectが入っているのが当然。
ところがEclipseにはWorkspace内のファイルが増えすぎると起動に失敗するようになり、こうなると設定ファイルを殆どクリアしてほぼ初期状態にしないといけないという厄介な問題が有りました。Projectが増えてきた時にWorkspaceの取り扱いはかなり面倒でした。

AndroidStudioではそれに比べてWorkspaceに相当するProjectの粒度がかなり細かいです。
Projectの切替は容易で、それどころか複数のProjectを複数のWindowで開くこともできます。
そのため、Projectは同じウィンドウ内に収めたくなるProjectという粒度で入れておけばよく、ライブラリーとそれを使うアプリや、Wearアプリとそれのコンパニオンアプリのように極めて関連性が高いModuleだけが同じProjectに入ります。
1Project内は1ModuleというProjectも多い、と言うかほとんどその状態になると思います。

ショートカットが凄い

ショートカットというとキーボードのショートカットキーを思い浮かべるでしょうが、それだけじゃなくAndroid Studioには様々なコードを簡単に記載する仕組みが用意されています。
Live templateを使うとよく使うコードを簡単なショートカットで入力できるようになります。

例えば、List itemsという変数があった場合、
items.for
と入力すると
items.for、fori、forrがサジェストされ、forを選ぶと自動的に

for (Item item : items) {

}

が入力されます。
複数形の変数名だったらちゃんと単数形の変数に置き換えてくれます。(あるいは型に基づく名前にすることも出来ます。)

foriだと

for (int i = items.size() - 1; i >= 0; i--) {

}

で添字iでループを回せるようになり、

forrだと

for (int i = 0; i < items.size(); i++) {

}

でforiの逆順となります。


同じように
notnullとすると

if (items != null) {

}

が入力されるという具合

独自のLiveTemplateを作ることも出来ます。
よく使う書式があったらコードを選択してTools-LiveTemplateを選び、Template名を入れるだけ、
あとはAbbreviationにTemplate名を入力するとサジェストされるようになります。
Edit variablesを選ぶことで動的に切り替えたい部分の指定も可能です。

もちろんショートカットキーも充実しています。
その中でも一番のお気に入りがshift二回押し。
これによりファイル名を入力して直接ファイルを開くことが出来ます。もちろんこの時もサジェストの時と同様、柔軟な指定方法が可能。
ツリー構造を辿る必要がなくなり数倍早くファイルを開くことが出来ます。

リソースのプレビューが便利

リソースで色や画像、文字を取得するとコード上に表示されます。

色がぱっと見えることでリソースの指定を間違えた時にすぐ気づけるほか、多少コードが長い場合でも即時にどこでリソースを取得しているかが一目瞭然です。

デバッグが便利

Android Studioのデバッグは機能面が強化されています。
特に便利に思うのが変数が変わった時に、どの変数がどのように変わったのかをインラインで表示してくれる機能です。
いちいちどの変数を取得するかを選んだりする必要はありません。


また、メモリーやCPUの使用量を時系列で見ることが出来ます。
これによりメモリーリークや、一番メモリーに負担がかかる処理を発見しやすくなりました。

かっこよく目に優しい

冗談のようですが私がAndroid Studioにして一番気に入っているのがかっこよさです。
ThemeにしてDarculaを設定した時のかっこよさはまるでAdobeのツールのようで少なくとも今までのIDEには見たことがないものです。
コードの色付にしても原色を使うのではなく全体のバランスを考えた色合いになっていてEclipseやNetBeansをカスタマイズしてもこのかっこよさには到達することが出来ませんでした。

カッコいいなんてアプリ開発に何の意味もないじゃないか? と思う人もいるかもしれませんが、2つのメリットが有ります。

まず、かっこよさがモチベーションにつながりました。
いかにも高度なことをやっていると思わせる美しいデザインと、ノイズの少ない没入感が高いデザインにより丸一日コーディングした時の集中力が増したことを感じています。

もう一つのメリットは目に優しくなったこと。濃い色と淡い色の組み合わせは全体の発光量そのものが抑えられるので長時間見た時の疲れ目が断然減りました。

最後に

ここまでEclipseと比べた場合のAndroid Studioのメリットについて紹介してきました。
もちろん、これだけでなくAndroidStudioには様々なメリットが有ります。

一つ弁解しておくと、これはEclipseとAndroid Studioの比較ではなく、あくまでEclipseに対してAndroid Studioの良いところだけを紹介しています。
もちろんEclipseの方が良かったところもあります。

ただ、Androidアプリ開発に於いてはEclipseとAndroid Studioのメリットデメリットを比較してどちらが良いかを選ぶフェーズではなく、新規プロジェクトは問答無用でAndroid Stuido、既存の保守アプリに関してもなるべく早くAndroid Studioに移行しないといけない状態です。 そのため、一方的にAndroid Studio側だけを賞賛する内容になっていることはご了承ください。

ちなみに、Google側の意向は無視したうえで、AndroidStudioに移行してよかったかどうかというと、私は100%満足しています。
2年前に移行した時は面食らいましたが、去年から特に制約がない限り殆どをAndroid Studioで開発するようになり、数ヶ月で慣れてしまいました。

神機の予感YAMAHAのAVアンプRX-V479レビュー

$
0
0
YAMAHAのAVアンプRX-V479を買いました。
YAMAHAのAVアンプを買うのは初めてですが、気が利いているな と思える部分があったので購入。
結論から言うと、これは買って良かったです。


今まで使っていたのはDENONのAVC-1570で、もうすぐ15年ちかく使っていました。


かなりヘビーユースしていました。
購入当初はノイズが殆ど無く、蚊の飛ぶ音くらいまで音量を落としても歪みない音楽が聞こえるのに感動していたのですが、
去年の終わりくらいからホワイトノイズが出るようになってり、今年の夏からは再生中に突然マイク端子を直接指で触った時のようなかなり強めのノイズが出るようになってしまいました。
分解してみたところ、半田のヤケでショートしている部分があったり、多数のコンデンサーが液漏れをしていたりとひどい状態、長くはないだろうと思っていたらついに先日電源がつかなくなってしまいました。

ということで買い替え

アンプの使用方法としてはPC用の音源を出力するのが9割、たまにスマートフォンやタブレットの出力を行うという使い方です。
なので、実はUSB-DACやプリメインアンプのほうがいいのでは?という気もしていましたが、最近Google MusicでBGMを再生することが増えたのでChromeCastを接続できるHDMI端子付きのAVアンプを買うことになりました。

今回は予算を5万円未満と決めていたのでAVアンプのローエンドに対象を絞りました。
AVアンプの場合、プリメインアンプに比べてハイエンドとローエンドの音質差があまりないのが特徴で、対応するデータ形式や接続できるスピーカーの数に違いが有ります。
PC繋いで2台のスピーカーを流している現状はもちろん将来的にスピーカーを増やしたとしても5.1chを超えることはないでしょうしローエンドで十分な感じ。

候補に入れたのは
YAMAHA RX-V479(B)
SONY STR-DN850

当初の予定ではSTR-DN850にするつもりでした。
SONYの良いところはなんといっても先進的な機能に貪欲な点で、例えばNFCを使ってBluetoothのペアリングを完了するみたいな便利機能が豊富にあります。
我が家にもSONY MDR-EX31BNSONY CMT-BT60がありますが、複数台のペアリングをかざせるだけで完了できるのはとても便利。

SONYのローエンドといえば、かつては高音は割れて低音は歪む印象でしたが、最近は随分と改善していて十分他社と競合出来るレベルになっています。

けれど結局RX-V479にしてしまいました。
選んだ理由はHDMIの入力端子が多いとか、YAMAHAのAVアンプは音質面が優れているとか色いろあるのですが、一番は前面にあるSCENEボタン。


これ、BD/DVD とかTVとか書いているのでINPUTボタンに見えますが、印字は意味がなくて、実際は設定を保存できるボタン。
長押しすると現在の状態を保存して、
次回からそのボタンを押すと設定を復元することが出来ます。
入力系統や音質設定を保存させるので例えば、BD/DVDボタンにPC、TVボタンにChromeCastを設定しておくとが可能。
さらには同じ入力系統でも、映画用の重低音重視な設定と技術動画用のボーカル重視な設定を別に割り当てるなんてことも可能。
電源が切れている状態でもボタンを押すと自動で電源が付くので、すごく楽ちんです。

普通のAVアンプだと、
電源をつける。
再生したい入力系統が出るまでINPUTを連打する。(リモコンなら一発で選べるものもあるけれど、本体のボタンだと順番に流れてくるのを選ぶしか無いのが殆ど)
イコライザー、低音、高音を選ぶ。
という手順が必要だったのが1回の操作で可能に。これは快適過ぎる。

そもそも、AVアンプ、使う入力系統は限られているし、音質の設定も、細かく設定できて欲しいけれど一度設定した内容をコロコロ変えることはないので、ちゃんと保存して、これを本体の一番大きなボタンでワンタップで切り替えられるのはとても便利。
きちんとUX考えてあるなーという印象を受けました。

同様の機能は更に下位モデルのRX-V379にもあるのですが、実売価格で1万円も差がない状態でHiRes、Wi-Fi、USBポートに対応してHDMIポートが多く、HiResAudioにも対応するなど機能差が大きいのでRX-V479はかなりお買い得な感じがします。
とくにUSB端子部分、RX-V479は横長の広いくぼみにAUX inと並ぶかたちですが、RX-V379はUSBに対応しておらず、端子もありません。なのにRV-V479と同じサイズのくぼみがあってUSB端子部分がないという状態で、いかにもコストダウンのために上位モデルの筐体を使いまわして機能を削ったというのがみえみえでカッコ悪い

その点RX-V479はデザイン上の破綻を感じません。



サイズはAVC1570と幅はほぼ同じ、高さは若干高く、奥行きは若干短いです。
このあたりは、技術の進化で基盤のサイズを抑えつつ、音質に影響を及ぼす排熱性能を重視している印象。


AndroidやiOSアプリを使ってリモコン操作ができるのですが、音量の上げ下げと言った単純機能はもちろん、音質の設定や電源のON/OFF(そうONも可能なんです)、エフェクターの設定、スピーカーの設定など殆どの機能を操作可能。


RX-V479はAirPlay(音声のみ)にも対応していて、iPhoneやMacの音声をWi-Fi経由でロスレス送信できるのも嬉しい。
D/A変換がAVアンプ側で行われるので有線ケーブルをつなぐより高音質です。AirPlayは常時待ち受け状態になっていて、再生側が再生を開始するとAVアンプの電源が自動的に入るのも嬉しい。

最近はCDを買わずにYouTubeで音楽を聞くという人も多いでしょうが、YouTubeの再生も(音声のみですが)対応しています。
AirPlayでの再生遅延は数秒あるものの、YouTubeの映像側が合わせて遅延させてくれるのでPC側の映像と音声がずれるということもありません。(しかし、ボタン操作時のレスポンスは一瞬劣る)
映像に関してはChromeCastのHDMI接続なら映像ごと送れるようになるはずなので第二世代ChromeCastが日本で発売されるのを待って試してみようと思います。
通信の環境かもしれませんが、曲の開始時に音飛びすることが有ります。

肝心の音質ですが、YAMAHAらしい癖のない透き通った音を出してくれます。
管楽器やサックスなどの音が心地よく感じられます。
かつては、この価格帯はONKYOが飛び抜けて音質がよく、YAMAHAは10万円クラスで音質が良かった印象ですが、ちょうどその10万円クラスの音質が降ってきたような感じがします。

あと、このRX-V479、元の値段は6万円超えでローエンドというよりミドルレンジに足を突っ込んだ価格帯なんですが、実質後継機のRX-S601(出力が若干落ちて省エネ)が出たおかげか、Amazonで約35,000円という、完全ローエンド価格帯まで落ちていて、機能を考えると意味不明な割引率になっています。

ここがダメ

ほぼほぼ満足しているんですが、一部に不満もあります。
まず、AirPlayに関して言うと、Macか、Wi-Fiの問題かもしれませんが、音飛びすることが有ります。
あと、DLNAサーバーの音楽を再生できるのですが日本語に対応しておらず、曲を選ぶのも大変です。
リモコンはほんと、コストカットの塊という感じでデザイン、質感、剛性どれをとっても物足りないと言わざる負えない。
(手元のスマートフォンで殆どの作業ができるのでリモコン使わなくてもよかったりはしますが)
スリープタイマーを設定していると、設定時間に突然 ぱちっという音とともに電源が切れてしまう。 本当に寝ようとしている時にいきなり音がなくなると逆にびっくりして目が覚めてしまいそう。フェードアウトしてくれたらよかったのに

おすすめ度 90%(かなりおすすめ)

6ポートも4K 60Hz対応のHDMIポートを備えてスマートフォンを始めとした外部連携も優秀、そして音質もかなりよく、これはかなり良い買い物をしました。
耐久性は今後の使用を試してみないと分かりませんが現時点では買ってよかったという気持ちしかありません。
それでいてこの実売価格はもはや意味不明なレベルです。

PC用バックパックThule Paramountショートレビュー

$
0
0
Thule Paramountデイパックを買ったのでどこまで詰め込めるのか試してみました。

今まで使っていたのはPorterの肩掛けバッグ。
完全にビジネス用途向けで、お硬い場所へ行くのもなんの問題もないデザイン。
軽い雨ならちゃんと防水してくれるし、そこそこプロテクト効果もあって使いやすい。
MacBookAirを入れるとこんな感じで、左右前後はプロテクトしてくれます。


しかし、肝心の底面にプロテクターがなく、布一枚挟んで地面です。
MacBook Airを入れると重さでかばんの形が歪んでMacBook Airのカド部分が外から分かるような状態に。 これはまずい


さらに最近MacBook AirをMacBook Pro retina15インチに買い換えて肩掛けでは重さを感じるようになりました。

サイズ的にはバッチリなんですが


書類をいれることを想定した作りということもあって、ACアダプターやスマートフォンを入れるとすぐにパンパンに膨れ上がってしまうこともマイナス。

そこで登山用のバックパックに変えたところ、たくさん荷物は入るし、体感重量は減るしで凄く楽になりました。
ところが、登山では軽量化が優先されるので缶詰やテントのようなそこまでプロテクターが必要ないものはとりあえず運べればよくて。
衝撃に弱いものは寝袋などで囲む。防水が必要な物はジップロックに入れるという感じで、プロテクターが必要な物を荷物全体を使って最小限守るという発想でバックパック自体にプロテクター効果がありません。
実際、カメラを入れていたところ、液晶が割れてしまったり、防水がないので雨が降ると途端に行動に制限がかかるという感じで割りと不便でした。
収納容量も大きいのですが、寝袋や鍋のようなかさばるものが入るようにだだっ広く、ACアダプターを入れておくスペースみたいなものもありません。
ということで、ちゃんとしたPC専用のバックパックを買うことにしました。

候補になったのは
Amazonベーシック デジタル一眼レフ/ラップトップ用 バックパック

Amazon BasicはUSBケーブルが使いやすかったのでかなり印象が良かったのと、開けると内装が派手なAmazonカラーなのが面白そうなこと、PC収納スペースと別にカメラ用関係の収納が充実していて小物が多いスマートフォン収納にも役立ちそうな所がポイント。

値段もかなり安いので多くを求めなければこれで良さそうです。


もう一つがThinkPad プロフェッショナル・バックパック
ThinkPadブランドのバッグというだけでも面白いのですが、背中側に隠しポケットがあって財布やパスポートのような貴重品を収めておくスペースが有るのが魅力。
これであれば海外でよくあるスリやカッターで切って中身を取ってしまうみたいなものからもプロテクトできます。
値段が安いのに、ビジネスでも通用しそうな落ち着いたデザインやトラックポインターを意識した赤いアクセントもオシャレ。
ノートパソコンのプロテクトもしっかり考えられており本体が周辺に接触しないハンモック構造を採用。
ターガス社のOEMということで、ちゃんとしたメーカーの製品なんですが値段もかなり手頃

物珍しさともともとThinkPadが好きだったことも有り最初はこれに決定


が、届かない。
注文がうまく行っていなかったのか、なんなのかわからないですが、一ヶ月待っても商品が届かないので、これは縁がなかったのだろうということで別のバッグを探すことにしました。

そんなときに見つけたのがカズちゃんねるのこの動画

そしてそこからリンクが貼られていたうしぎゅーさんのBlogオマスキャ

あ、なんか良さそうということで、こちらをポチってみました。


かずちゃんねるやオマスキャのサイトではおしゃれさが評価されていましたが、個人的に感じた魅力はそこではなく、外向けに開かれたポケットが少ないということ。
私結構おっちょこちょいなので、気がついたらバックパックのチャックが開いたままだったということが何度もあります。

PC用のバックパックはチャックの数が増えるので余計にそういうことが増えそう。
その点、このバックパックは大きなフラップがあるので、側面のチャックさえ気をつけておけば開けっ放しになってたということを防げそうです。

ということで、届きました。

第一印象は軽い! でかい!
見た感じもっとずっしりしているのかと思ったのですが、普通な軽さでした。

サイズ的にはPorterより一回りほど大きい感じだけれど、角ばったボディもあって体感的にそれ以上に大きく見えます。



想像していたより質感が安っぽい感じ。全体的にプラスチックとジャージです。
バックパックに必要なのは見た目より実用性なので必要十分ですが、紹介記事にあるようなおしゃれ感はあまりなく、おしゃれさを求める人はIsarとかのほうが良いと思います。
Evernoteモデルだとベルトが緑になっていたりしてかなりオシャレです。


ただ、安っぽいけれど安い作りかというとそうではなく、肝心の縫い目やクッションはしっかりしているし、変な装飾がない分、安さと軽さにつながっていると思えば実用性重視な業務用としてはむしろありがたいです。

さて、本格的なレビューは実際に長期的に使いながら判断してみたいのですが、まずは、ガジェット類をどの程度詰め込めるかチャレンジしてみたいと思います。
私は本職でスマートフォンアプリ開発をやっているので、PCは常時携帯、それに検証用の端末を複数台持ち歩いたりします。
今までだと肩掛けの時もバックパックに変えてからもMacBookPro Retina、Nexus5、Nexus6、Nexus9の4台が限界という感じでした。
これも、かなり無理していて徒歩が多い時はMacBookPro Retina+Nexus5、Nexus6の3台構成がほとんどでした。

それに対して、このバック収納力がかなりありそうなので本気で積むとデスクトップ以外自宅の全モバイル機器が入ってしまいそうな勢いです。
そこでレギューレーションとして、全ての端末が個別のポケットに入っていて固定された状態になることとしました。
つまりガジェット数の上限はポケットの数となります。

まず、フラップを開けてすぐのところに小物入れがあったのでNexus6を入れてみました。
Nexus6はファブレットの中でも大きめなので収納に苦労すると思っていたんですが、さくっと入ってしまいました。

この中にはもう一つポケットがあったので、iPodTouchを入れてみました。


次にメインスペースです。
一番背中に近いところにはPCを入れるためのスペースが有ります。このスペースは外側からアクセスしやすくなっているだけじゃなく、クッションも一番充実しています。 MacBookPro retina15インチを入れてみます。

今時のノートとしてはかなり大きいですが、すっと入ってしまいました。
その手前のスペースには、このバッグ最大の収納スペースが有ります。着替えとかデジカメとかACアダプターとか入れておくのに便利そうですが、今回はMacBookAir13インチを入れてみます。
言うまでもなく余裕で入ります。 注意点としてはここは唯一、底まで通じているので、一番下にある荷物は地面とゴム一枚挟んで接することになります。
Porterよりは衝撃を吸収してくれそうな作りですが、電子機器を入れる場合は内部で別にクッション材を入れておいたほうが良いと思います。


この収納スペースの奥にはタブレットを入れておくための小さなポケットが有ります。
Nexus9が余裕で入りました。


その手前にはポケット多めの小物を入れておくスペースが有ります。
外側のポケットにHT-03Aを入れてみます。もっと大きな端末も入りそうですが、手元の端末がだいぶ少なくなってきました。
HT-03Aを使っている人は流石にもう居ないと思うのですが、多環境対応を求められた時のボトムエンド検証端末として今でも現役で使っています。
使い始めて6年目にも関わらず未だにバッテリーが生きているのが素敵です。

奥の大きめのポケットにはNexus7(2012)が入りました。
2012年モデルは割とズングリムックリしたサイズなのです、それでも十分に収納。


ポケット以外の部分については収納スペースがかなり大きく、普通にノートパソコンのChromeBook Pixelが余裕で入りました。


その奥にある2つのスペースには
GalaxySIIIとNexus5をセット


一番外側のポケットはきつきつですが、それでもNexus9が入りました。


外側にあるペットボトルホルダーにはiPhone5を入れてみました。
言うまでもないですが、ここはプロテクトが全く無い野ざらしかつ、外から取り放題なので実際にここにスマートフォンを入れることはあり得ないですが、モバイルバッテリーとかを突っ込んでおくと便利そうです。


最後にフラップに非モバイルながら、ChromeBoxを入れてみました。


ここは幅はそこそこあるけれど奥行きがないのでMacBookAirの13インチを入れると1/3くらい飛び出した状態になってしまいました。
もちろんChromeBoxはすんなり入りました。

とういことでおさらい
全部でこれだけ入りました。

ノートパソコン3台
デスクトップ1台
タブレット3台
ファブレット1台
スマートフォン4台
MP3プレーヤー1台

これだけ荷物を入れてもバックの形がよがんだりせず、普通に元のシルエットを保っているのが素敵です。
他人が見ても、この中にこれだけの数が入っているとは思わないでしょう。
これは内部のポケットでものが立体的に収納されていることと、しっかり固定されているので外側に傾いたりしていないのが大きそう。

総重量(バックパック込み)で9kgでした。
流石に持つときは質量を感じます。しかし、一旦背負ってみるとなんとかなりそうな重さです。
これで一日出歩けと言われると嫌ですが、出来ないことはないです。
重さの割に持ちづらさを感じません。

MacBookPro retinaが体に一番近いところにあるので、背中に当たり痛みを感じるかと思っていたのですが(登山用ではかなりそれが辛かった)、若干浮いた構造になっているのと分厚いプロテクターのおかげでからってみても、違和感が全くありませんでした。

体感の重さって、体の重心に重量物がどれだけ近いかでかなり変わります。(ペットボトルを体の近くで持つのと手を前に伸ばして持つ時のさをイメージしてください。)
Thule Paramountは重量物のノートパソコンが体に一番近い場所にある上に、それ以外の荷物も上下方向に立体的に収納されるので重心に近いところに収まっていて、加えて機材が固定されているので歩いてみても機械同士がぶつかったりする不安な感じがなく、重心がぐらつかないので体が引っ張られることが無いのも体感的軽さにつながっていそう。

登山用ほど、体全体で支えるという構造にはなっていませんが、肩紐もそれなりに広く紐が広がらないようにするための簡易的な紐も付いていてそれなりに荷重が分散されているようです。

これだけ持ち運ぶのが楽だったら、もう少し荷物を増やしてもいいかもl
あと、海外カンファレンスでも、行きはこの中に全部詰め込んで機内持ち込みすればロストバゲージのようなリスクもなくせそうです。

SlimBradeからIntuosに乗り換えてみた

$
0
0
いろんなマウスを探していて、結局ロジクールのVX nanoとG5が疲れにくさの点で最高と思っていたのですが


去年の終わりからケンジントン SlimBladeを使っていました。


詳しい経緯は以前の投稿にかいてますが、もともとはMacばかり使うようになって、Windowsをあまり使わなくなったので、Windowsのためにマウスの作業スペースを確保したくないと思って省スペースのために導入したもの。
ものすごく使いやすくて肩こりも激減したあげく、Windowsの方が使いやすい! と思えるほど使い勝手が良くなったので、USB切替器を使ってMacでも使うようになり、今年になってからはマウスを全く使わないPCライフに切り替えていました。

トラックボールのメリットとしては、マウスと違って位置が固定されるので腕の位置を最適化出来ること。
そこに行けば必ずあるので、目線を動かさずにすぐつかめるのも良いところ。なのでキーボードとマウスを頻繁に持ち帰るような使い方には最適です。
また、きちんと肩に負担がかからない場所と高さを探してそこにおいておけば肩こりが激減します。

カーソルを動かすときに、物をすべらせるのではなく、玉を転がすだけというのもメリット。圧倒的に必要なチカラが少なくて済みます。
マウスを使っていた時には気にしたこともなかったのですが、トラックボールに変えてからいかにマウスを動かすときに無理な力が腕にかかっていたかに気付かされました。

ところが、トラックボールにも弱点があって、細かい部分を選択するのが苦手。
カーソルをぐわっとおおまかに動かすには良いんですが、イラストを書いたりするような細かな選択が繰り返し行われる場合はマウスやペンタブレットの方が便利です。
そこで、通常利用はトラックボール、デザインはペンタブレットという使い分けをしていました。

使用していたのはWacom のIntuos Pen & Touch。
wacom Intuos Pen & Touch medium

これももともとは、Windowsのマウスを置き換える目的で買ってたんですが、タッチパネルの精度が期待したレベルではなくタッチ・パネルをオフにして、ペンだけを使っていました。

ところが、MacをEl capitanにアップデートしたらこのペンタブを認識しないというトラブルに遭遇。
どこをタップしても一番左上にカーソルが飛んでしまうので全く使い物になりません。

その後、無事にドライバーがアップデートされてEl capitanに対応。
タッチも認識するかな?と何気なくためしてみると、

あれ?すごくなめらかになってる。
MacBookPro本体のタッチパッドと遜色ないレスポンスで使っていてストレスが全然なくなっています。
もちろん、マルチタッチのジェスチャーも認識して動きも滑らか
二本指でのスクロールやピンチイン・アウトもらくらく。
これはもしかして常用レベル? ということでSlimbladeを封印して今はIntuosのタッチ・パネルをメインで使ってみています。
1日使った印象としては多少問題はあるけれど十分使えそう。このまま行くとSlimbradeが封印されるかもしれません。

良いところ

疲れにくい

指をすべらせるだけなのでSlimbrade以上にカーソルを移動するのに力を使いません。
大雑把にカーソルを動かしたいときはSlimbradeの方が楽なので、トータルで言うとSlimbradeよりは若干疲れるけれど、マウスに比べたら圧倒的に楽という感じ。

マルチジェスチャーに対応していて、スクリーンの切替などが簡単。

Slimbradeでは第三ボタンをMission Controlに割り当てていたのですが、Mission Controlでスクリーンを切り替えるには、第三ボタンを押して、スクリーンを選ぶという二度手間が必要だったのが、スワイプ一発に変わったので圧倒的に早い。
El capitanではMission Controlでスクリーンを選ぶのがウィンドウ上部の小さな領域に変わってしまったので余計にこの差が大きいです。

ペンも使える。

イラストやデザインなどでより繊細な操作をしたいと思った時はすぐにペンに持ち替えることが出来ます。
いままではマウスならマウスパッド、Slimbradeの時はSlimbrade本体をどかしてペンタブを引き出していたのですが、そういう手間が全くなくなってペンを取るだけでいつもの作業位置ですぐにデザイン作業を始められるのは便利。

ペンの精度は一昔前のIntuosレベル。

Intuosはブランドが変わって従来のFavo、BambooラインがIntuosに、従来のIntuosラインがIntuos Proになったのですが、新しいIntuosでも、古いIntuos3と遜色ないペン精度がありました。(その辺りの自信があるのでIntuosのブランドを引き継いだのでしょう)
FavoやBambooの精度の悪いペンに絶望していた人にとっては別物であるということをお伝えしておきます。
ペン全体の作りでいうと従来のIntuosより安っぽいですが、ペンをメインで使うデザイナーさんとかでなければこれで十分だと思います。

細かい選択がしやすい

Slimbradeはもちろん、マウスと比べても細かい部分が選択しやすいように思います。

ここがいまいち

ドライバーが不安定

スリープ解除時にタッチパネルだけ認識しなくなることが何度かありました。
タッチ・パネルをOFF/ONにすると復帰するのですが、ちょいストレス。

ジェスチャー操作がMac本体とちょっと違う。

何故こうなったレベル。
例えばスクリーンの切替がMac本体だと3本指のスワイプですが、Intuosだとドラッグに割り当てられていてスクリーンを切り替えるには4本指のスワイプになっています。
外に行くときはMac本体のジェスチャーを使うのでこの差がプチストレス。
カスタマイズも殆ど出来無くて、Mission Controlを呼び出せないのがわりとストレス。
付属ボタンにCtrl+↑を割り当てることでMission Controlを起動するようにしていますが、やはり4本指上スワイプで開いて欲しいところ。

大きすぎ

これは欲張ってMサイズを買ったのが悪くて、Sサイズにするべきでした。安いし
あと、全面タッチパッドみたいな見た目ですが、実際には外周3cmくらい反応しない領域があって あれ?となることが多々。
Intuos proだとこの部分が段差になっているんですが、Intuosでは凄く小さな点が4隅についているだけなので、触って反応せずに あれ? となること多々。
写真の中央部分に点があるのですが、これが認識するエリアの目印。 見えづらすぎ。


表面がざらついている

このざらつきのおかげで、ペンを使っているときはまるで紙に書いているような感覚を味わえるのですが、タッチパッドで使うときには触り心地を損ねています。
MacBookのガラスパッドをイメージしているとちょっと残念な気持ちになります。

おすすめ度 50%

いくら精度が良くなったとはいえ、触り心地、枠の問題、ジェスチャーが純正と違う問題でApple純正のTrackpadほど使いやすいとはいえないです。
最大の魅力はペンが使えることで、この魅力はTrackpadでは得られないもの。
Magic Trackpad2が大幅値上がりしたのと、Intuosは型落ちモデルの価格が安くなっていることで、かなり安くてに入ることも魅力。
有線接続の信頼性、付属アプリが豊富なことも魅力に感じる人はいると思います。

Magic Trackpad2の税込みで15,000円を超えるのは高すぎで正直誰にもおすすめできないレベルですが、型落ちのMagic TrackPadがまだ残っていて割と安くなっているので、ペンに魅力を感じる人はIntuosそれ以外だったらMagic Trackpadのほうが良さそうな気がします。




プチ便利アイリスオーヤマのクリアケース

$
0
0
最近アイリスオーヤマのクリアケース(A4深型)を使っていてプチ便利だったので紹介します。


モバイルアプリ開発をしているとどうしても小物が増えてしまいます。
これまではそういう小物をかばんに納めていたのですが、かばんの奥深くにあるものが取り出しづらかったりして、モバイルバッテリー持ってくるの忘れたー と思って家に帰ったらかばんの奥底にあった。なんてことも多々。
ということで、グッデイで売ってたアイリスオーヤマのクリアケースを使っています。
アイリスオーヤマ ケース クリアケース CC-304S A4 浅型 仕切り付き


ご覧のとおり、特にこれといったへんてつもない普通のクリアケースです。
この中に小物類を収納しています。

中に入っているのはこんな感じ
中の物はその時の用途や使用状況により細かくアップデートしています。

USBケーブル

USBケーブル長 2本、USBケーブル短 2本(うち1本は高速充電用)

なんといってもアプリ開発で一番大事なのがUSBケーブル。
全体で4本ありますが、長いUSBケーブルはスマートフォンを手に持って開発する時(加速度センサーが必要なときとか)やポケットにモバイルバッテリーを入れた状態でスマートフォンを手に持ちながら充電する時などに使います。
AmazonのものとAnkerのものが2本入っていますが、長いケーブルはあまり使うことはなく優先的に短いケーブルから使うことが多いです。
短いケーブルのうち1本はSmartWatch2に付属していたケーブルで15cmしかなく、卓上でPCとスマートフォンをつなぐのにとても使いやすいです。
特段特徴がないケーブルですが、電気抵抗は長さに比例して増えていくので短いほうが充電効率も良くなりそうです。

残りの一本はルートアールの20cm
通信ができない充電専用で、給電側の事情関係なく強制的に高速充電になるので、若干注意が必要ですが、モバイルバッテリーと組み合わせるとものすごい速度で充電が完了します。

USBケーブルはMacBookPro本体のUSBケーブル2個と充電ポート2個の4ポートあるため4本持ち歩いています。(実はモバイルバッテリーがあるので実際には5台同時充電が可能なのだけれど)


USB充電器

Anker 20W 2ポート USB急速充電器
充電界隈では非常に人気が高いANKERのUSB急速充電器です。その中でも最もコンパクトな2ポートタイプを持ち歩いています。
ポート数は多くないですが、電源端子が収まってコンパクトに携行できるので常備しています。最近より高出力になった後継機やポート数が多い4ポートタイプもでたようで気になっています。


モバイルバッテリー

Anker Astro E1 5200mAh日頃ノートPCを持ち歩いていることも有り、何かアレばノートPCから取ればいいやという感じでモバイルバッテリーを使っていなかったのですが、去年頃Nexus5のバッテリーが異常消耗する症状に見舞われて毎回PCとつなぐのもストレスなので初購入。
そういう経緯もあるのでバッテリー容量は少なめで軽さを優先しました。
しかしながら、これでもNexus5を満充電するには必要十分で、しかもその充電速度が凄く早い。
特に抵抗が少ないことで有名なルートアールのケーブルと組み合わせるとコンセント以上に高速充電されるので便利です。
しかし、この組み合わせだと、充電が早過ぎるのか、スマートフォンがものすごい発熱するので電源を消しておくなり重たい処理をしない状態で風通しのいい場所においておかないといけないです。
その後Nexus5がアップデートされて電池持ちが良くなったことも有り、最近ではあまり出番が無いです。

電源タップ

プラグ収納式 マイクロタップ
イベントに行くと電源ポートが限られていることがあるので、他の人に迷惑にならないように電源タップを使っています。
コードがないので手に収まる小ささながら、4ポートもあるので必要十分。
古いホテルとかでテーブル上に電源がなく、テーブルスタンドを抜かないといけない みたいなときも便利。
あと、180度回転させられるのでMacのACアダプタ挿したら隣とぶつかる みたいなときにACアダプターをずらすのにも使えます。


LAN関連

Thunderbolt Ethernetアダプタ
ホテルの中にはwi-fiが飛んでおらずLANだけ使える場合などがあるので、その時のためにThunderboltに繋いで使えるEthernetアダプタとLANケーブルを持っています。
普通、この手のはUSB接続が多いのですが、2本しか無いUSBポートを消費したいですむThunderboltタイプが便利です。LANケーブルは取り立てて特徴がない、爪折れ防止がついたLANケーブルです。
ホテルでは盗難防止のためにLANケーブルをフロントに取りに行かないといけない場所も多いので自分で持っておくと便利です。


変換ケーブル

AndroidにつなぐとHDMIを出力できるMHL変換アダプタと、USBホストになる変換アダプタを持っています。
どちらも使うことはあまり無いですが、軽いのでとりあえず入れている感じ。

折りたたみドライバー

iHelp カラビナ付折りたたみドライバー
これもあまり使うことはないですが、ちょっと何かを直したいとかいうときに助かります。
構造的に本格的なドライバーほどテンションを掛けることが出来ないですが、このコンパクトさでプラスマイナス計6本使えるのは助かります。


Bluetoothイヤホン

SONY MDR-EX31BN
最近やたらIT界隈でユーザーが増えている人気のBluetoothイヤホンです。
出かけるときはほぼ必ず携帯しています。家に帰ったら充電した後、このケースに入れていますが外出時にはポケットに入れておくことのほうが多いです。
これはいいものです。


で、こういうのをガチャガチャ持ち運んでいたのですが、やはり邪魔だなぁということでクリアケースを購入。

ここがいい

かさばりにくい

A4サイズと薄くて広い形のため、かばんに入れた時に収納量のわりにかさばりにくいです。

探しやすい

クリアケースなので開けてみなくても中に何が入っているのか確認できるのもありがたい。
あと、全て開けてみることができるので、探すためにあれこれ取り出す必要が無いのも嬉しい。

開け閉めが楽

一番気に入っているのはチャック類と違って、開け閉めの手間が簡単なこと、片手でもすぐ出来る。
出しやすくて収納しやすい。
イベントに行くと話が弾んで会場の閉鎖ギリギリまで粘ってしまい、いますぐ出てください! みたいな話になることも有りますが、そういう時も中に放り投げて蓋閉めたらそのままかばんに放り込めるのが素敵。
cafeでの作業のような短時間作業でもすぐに準備して作業してぎりぎりまで作業してすぐに撤収出来るのがありがたい。


イマイチなところ

深さが足りない。

かさばらないのとトレードオフなんですが、
ビミョーなサイズの違いでMacBookProの電源アダプターが(ケーブル巻いた状態では)格納できない。
あとmoto360の充電台も無理すれば入るけれど、かなり無理がある感じ。

柔らかさはない

かばんに入れる時に外のタブレットなどを傷つけないか気を使います。
上面側は良いんですが、底面側は足が飛び出しているのでちょっと気をつけたいところ。

おすすめ度 50%

私みたいに小物が多い人だったら90%おすすめします。
使っていた時の周りの反応としては半数はポジティブな反応ですが、
一方で残りの半分は、怪しい物を見る感じで見てくることが有ります。
それ魚釣りのケースですよね? と言われることもまぁよく有ります。
魚釣りケースとして使っても別にいいと思います。


大人が見てもハマる おさるのジョージの魅力

$
0
0
おさるのジョージが面白いよっていうので見てたんだけれど
こりゃ大人がハマるレベル。

子供向け番組って基本的にテンプレ大量生産方式が多い。
例えば子供向け番組の王者アンパンマンの場合、
バイキンマンがゲストを襲う。
ゲストを助けに入ったアンパンマンをバイキンマンが撃退する。
アンパンマンの顔交換。
バイキンマンを殴って解決。
基本的にはこの流れ。
たまに平和的解決方法で解決することもあるけれど、9割はこのテンプレに沿ってる。

おさるのジョージも厳密にはテンプレがあるんだけれど、
ジョージが遊ぶ→問題が起きる(起こす)→試行錯誤する→解決
という程度のざっくり感。

そして出てくる問題が凄い
公園の野外ステージが取り壊されるので、ライブ会場を探すとか
断水の中どうやってシャワーを浴びるとか
毎回出てくる課題が現実的ながら、独創的で難易度が高く、大人が見てもハラハラする。

その後の試行錯誤も見事で、問題を解決すると新たな問題に直面していく
断水の話の場合、井戸を掘ればいい といって街路樹の近くを掘り始める。
けれど水を風呂まで運ぶのにスポイトを使って時間がかかりすぎる。
長いパイプとスポイトを使ってポンプを作る→けれど井戸が枯れてしまうとか

この一つ一つ問題を解決していくところに説得力がある。
○○すればいいのに 猿だから思いつかない みたいな逃げ方をせずに、
視聴者が○○すればいいのに って思いつきそうなことをどんどんやって、新しい問題に直面していく。

解決の仕方も見事で、
ジョージも見ている方ももう他に方法はないんだろうかと諦めたくなったところで、外的要因でいい方法が見つかり解決する という流れ。

子供向け番組の場合、ここをキチンと描かず、ひっちゃかめっちゃかになったことを落ちにしちゃったり、
毎回同じ方法(ロボット出てきたり、光線打ったり)で締めちゃうことがあるけれど、おさるのジョージは試行錯誤の結果外的要因を巻き込んで現実的な解決方法できちんと解決する。

見ている側もジョージと一緒に悩んでいるので、それが解決して、あぁよかったと素直に共感できる。
作品が毎回すごく練りこまれて 愛情持ってストーリーが作られているなと感じる。

huluやamazonプライムのような定額ビデオでも幅広く配信されているし、NHKによる放送もあるので気軽に見られるのも嬉しい。

イジェクトが無いキーボードでMacをスリープさせる方法

$
0
0
Macではオプションキーを押しながらイジェクトボタンを押すことですぐにスリープ出来る機能があります。
とても便利でした。離れた2つのキーを押すというのも直感的かつ押し間違えがなくてよかったです。ThinkPadのLED点灯と同じ発想ですね。
ところが、最近のMacBookシリーズはイジェクトボタンがなくスリープのショートカットキーが使用できません。

ディスプレイを閉じればスリープになるのですが、クラムシェルやデュアルディスプレイで使用している時はスリープのためにわざわざ画面を閉じるのは非現実的。


MacBook本体のキーボードには電源ボタンがあるので1.5秒以上押すことでスリープも出来るダイアログが表示されるのですが、ショートカットキー一発に比べるとなんか面倒だし、外付けキーボードではそれも使用できません。


実はMacは新しいシュートカットキーを簡単に追加することが出来ます。
これを使ってControl+F12にスリープを割り当ててみます。(Option+F12に割り当てることは出来ません。)

システム環境設定を開き、キーボードを選びます。


ショートカットを選び「アプリケーション」を選択します。


+を押して
アプリケーションに「全アプリケーション」を選び
メニュータイトルに「スリープ」と入力
キーボードショートカットの上でControlを押しながらF12キーを押します。


これでControlキーを押しながらF12を押すと一発でMacがスリープするようになります。
「メニュータイトル」にはメニューバーに表示される項目を好きに設定できます。
システム終了を選べば電源をきることも出来ます。(確認ダイアログは表示されません。)
コピーのようなありがちなコマンドを登録しておけば複数のアプリでコピーのショートカットが利用できるようになります。(Copyと表示されるタイプのアプリでは使用できないのが残念ですが)

Macで格安に高音質なディジタル音声出力を行う

$
0
0
MacBookシリーズはノートPCとしては比較的良いスピーカーが付いているのですが、やはり本格的に音楽を楽しみたいとするとオーディオシステムと繋ぎたいという欲求が出てきます。
高音質でPCとオーディオシステムと繋ぎたいという場合、ディジタル接続が理想です。

Macでディジタル接続しようとすると、よく言われるのがUSBのオーディオインターフェイスを使う方法。
たしかに、USB接続すればPCのノイズをアナログ信号から離すことができるし、USBオーディオインターフェイスは一般的にMacBook内蔵のオーディオインターフェイスより良い部品が使われていて音質がアップします。

けれど、比較的新しいMacと、極端に古くないオーディオシステムを使っているならもっと手軽でもっと格安でもっと高音質にすることが出来ます。

それがTOSLINKを使う方法。
TOSLINKは光ファイバーを使ってディジタルで音声を転送します。光ファイバーなので電磁波などのノイズに影響を受けることもありません。
Windowsのデスクトップ機なら最初から内蔵しているものも多いです。

オーディオ側ではよくこういう形の端子になっています。


で、これとMacをどうつなぐの? って話ですが、
実は多くのMacについているイヤホンジャックはTOSLINKに対応しています。
オーディオで一般的な角形ではなく、3.5のステレオミニプラグと同じ形をミニプラグアダプタという丸型のTOSLINKです。
対応端末一覧

片方が角形で逆が丸型の変換ケーブルはAmazonでも安く売っています。


既に角形のケーブルが有る場合、角形を丸型にする変換コネクタも有ります。


変換コネクタをMacに挿してみると、光出力しているのを見ることが出来ます。


手をかざすとイヤホンジャック内部が光っているのがわかります


あとはオーディオと接続するだけでアナログ結線の時と同様にオーディオで出力することが出来ます。
音質はさすがディジタル接続だけあって、アナログ接続するよりだいぶ高音質です。(もちろんオーディオ側の性能次第ですが)
手元にあるUSBオーディオインターフェイスと比べても圧倒的に良くなっているのを感じます。

Macでディジタル音声を出力する方法としてUSBオーディオインターフェイスもよく紹介されますが、音質に拘ったモデルだとそこそこのお金がかかりますし、安物だと結局音質はそこそこ止まり。
一方でTOSLINKなら数百円のケーブル一本買うだけでオーディオまで全てディジタルで接続できノイズの心配が必要なくなります。
USBオーディオインターフェイスより耐ノイズ性でも有利です。AirPlayと違ってあからさまな遅延もありません。

もちろん、USBオーディオインターフェイスによくある、マイク入力やMIDI接続などの豊富なオプションは使うことが出来ませんが、単に高音質に音楽を楽しみたいというのであれば、こういう方法もお手軽で良いのではないでしょうか。



Android WearのWatch Faceを作る(概念編)

$
0
0
Android Wearが発売されて1年以上が経ち、段々と面白いアプリも増えてきたように思います。
ウェアラブルとしては後発名だけあって進化の速度は早いように思います。


特にWatch Faceという独自の文字盤を作ることが出来るようになったのがとても嬉しい。
私はRotary watchというロータリーエンジンをデフォルメしたWatchFaceをリリースしています。


WatchFaceを作ることで、自分だけのオリジナルな時計を作ることが出来ます。
それも、背景の写真を変えるとか、針の形を変えるなんていう制限あるカスタマイズではなく、完全に自由な発想をそのまま作ることができるので、時計の概念を崩すような時計を作ることも可能です。
また、時計として表示するだけでなく、対話的な挙動をつけたりSNSの新着を表示するような仕組みを組み込むことも出来ます。

今回の連載ではWatchFaceの作り方を、実際に作成しながら学んでいきます。
最終的にはカスタマイズ可能なアニメーションするオリジナルのアナログ時計を作成します。


注意すべき点

Watch Faceの作り方は基本的に自由です。
ただし、幾つかの部分で注意する必要があります。

開発環境はAndroid Studioを使う。

Eclipseを使って開発するよりAndroid Studioを使って開発したほうがずっと簡単です。
EclipseのAndroidサポート自体が終了するのでこれを機にAndroid Studioに移行してしまいましょう。

電池の使用量に制限がある。

Watch Faceを作る上で一番大きな問題が電池使用量です。
Android Wearのバッテリーはスマートフォンの1/5〜1/10程度しかないうえに、Watch Faceが実行されている時間は通常のActivityより断然多くなります。
そのためWatchFaceではバッテリーの使用量を抑える必要があります。

Android Wear側の仕組みとして、Watch Faceは表示された後数秒でスリープまたはAmbient Modeという省電力状態になります。スリープになるかAmbient Modeになるかはユーザーの設定によって変わってきます。
Ambient Mode対応については後に詳しく記載しますが、これによりWatch Faceを作るときは、通常時と別にAmbient Modeを意識した作りにする必要があります。

Watch Face側としては、じゃぶじゃぶと電力を使うことは許されずに消費電力の少ない=計算量の少ない 作りを意識する必要があります。

限られた操作性。

Watch Faceでは通常のAndroid同様にタッチイベントなどを拾うことが出来ます。
しかしながら、長押しはWatch Faceの変更、上方向からのフリックは簡易設定、下方向からのフリックはNotificationの表示、左方向からのフリックはランチャーの起動とシステム標準の挙動が設定されており使うことが出来ません。
また画面が小さいため、画面上に沢山のボタンを置くことも出来ません。
実質的には多くとも画面上4箇所程度のタップ判定を行う程度に抑える必要があります。

多環境対応。

もしWatch Faceを広く公開するつもりがあるのであれば、通常のAndroidと同様にAndroid Wearにも様々なハードウェアがあることに注意が必要です。
液晶画面には丸型と四角のものが有り、解像度も様々であることに特に注意してください。

画面サイズについてはスマートフォンとちがって、どれも大きく違いません。
厳密には小さな画面も大きな画面も有りますが、いずれにしてもUIの構造を変えるほどの差がありません。
そのため、多解像度対応についてはスマートフォンと異なるアプローチが取られます。

スマートフォンの場合は3.5インチ程度から6インチサイズまでありますし、タブレットまで同じAPKで対応するならそれ以上のサイズについても意識する必要があります。
UI部品は、画面サイズに応じてピクセル数を変えるdpi単位が用いられ、ピクセルサイズに応じて画像を切り替えることでどの端末でも同じような大きさのUI部品となるように作るのが一般的です。

Android Wearの場合はUI部品の大きさを揃えるよりも、画面いっぱいに表示されるようにUIを全て均一に拡大縮小するのが一般的です。
そのため、画像を画面密度ごとに用意するのではなくある程度大きめの画像を用意しておいてリサイズするという方法になります。

デザインを考える。

Watch Faceのデザインを考えます。 基本的にはどんなデザインも可能ですが、注意が必要なのがAmbient Modeの存在です。
Ambient Modeになると、液晶の色数が減り、バックライトが抑えられ、画面のリフレッシュレートが1分間あたり1回になります。
色数がどの程度減るかについては端末によって異なるようで、フルカラーの画像はGear Liveやmoto360では16色程度の色数となりました。
しかしながら、どういった色数になるかわからないことと、バックライトが消えてコントラストの低い文字は認識しづらくなることを想定すると、白黒+アンチエイリアスで作るのが無難かと思います。

リフレッシュレートが1分間あたり1回になるということは秒針の表現が出来なくなることを意味しています。
そのため、白黒でも通用するデザインかつ、秒針がなくなっても問題ないデザインを考えます。
その他にも振り子時計のようなアニメーション部分は再現が出来なくなります。
Ambient Modeの時だけ全く違うデザインにすることも可能です。

デザインをする上では、それが丸型や正方形、長方形の画面で表示されるかもしれないことを意識します。
丸型に合わせて、四角に表示された時にカド部分を余白にするのが楽かと思います。
丸型と四角で別々のデザインをすることも可能です。

画像を用意する。

Androidスマートフォンの場合、複数の解像度を用意するためにイラストレーターなどのドロー系ツールを使ってデザインするのが望ましいですが、Android Wearの場合それほど多くの画面サイズをサポートする必要が無いのと、端末側で拡大縮小するのが前提となるため、Photoshopのようなペイント系ツールでデザインすることも出来ます。(ただし、将来的により高解像度なWearが出た時に備えて実際に出力するより高解像度な画像を用意しておくことをおすすめします。

画像を用意するときには、Bitmapで出力する部分と、LineやTextなどプログラムで出力する部分を切り分けます。
基本的にはLineやTextで出力したほうがリサイズが発生しない分綺麗に描画されるようになります。
より多彩な表現を使いたい時にはBitmapが必要となるでしょう。

Bitmapで出力する部分については、背景のような動かない部分と、長針・短針のようにそれぞれ個別に動く部分を別々に切り出せるように意識します。
Android Wearではどのようなデザインにするのも自由なため 長針や短針という概念がない時計を作ることも可能です。

次回は実際にWatch Faceを作成します。

Watch Faceを作る 5分で作るWatch Face

$
0
0
Android WearのWatch Faceを作る 目次

今回からWatch Faceのテストを行うための環境構築を行って実際にWatch Faceを作ってみます。
前提として最新のAndroid Studioをインストール済みでスマートフォンの開発が可能な状態としてください。

テスト環境の作成

実機を使ってテスト環境を行う場合、Android Wear側もデバッグを有効にする必要があります。
デバッグを行うには、通常のmobileでよく使われるようなUSBによる有線接続と、Bluetoothによる無線接続の2つがあります。
一般的には有線が楽ですが、Moto360のように有線接続がないデバイスでは無線接続によるテストを行う必要があります。

開発者オプションの有効化

Wearでの開発者オプションを有効にする方法は基本的にMobileと同じです。
ランチャーから設定を開き端末情報から、ビルド番号を7回タップします。
開発者向けオプションが有効になったらADBデバッグを有効にします。
この時、PCとWearがUSBで接続されていたらデバッグを許可 が出てくるので
「確定」あるいは「このパソコンからのUSBデバッグを常に許可する」を選びます。

有線接続の場合は、これで通常のmobile同様にテストを行うことが出来るようになります。

無線debug環境の設定

無線接続の場合は、次にBluetooth経由でデバッグをタップして「有効」を選びます。

スマートフォンのAndoridWearアプリを開いて右上の歯車のアイコンから設定を開きます。
一番下にあるBluetooth経由のデバッグをオンにします。
ホスト:未接続
ターゲット:接続
となっていることを確認します。

スマートフォンとPCをUSBで接続して、PCのターミナル(コマンドライン)にて次のコマンドを実行します。

adb forward tcp:4444 localabstract:/adb-hub
adb connect localhost:4444

Wear側にデバッグを許可が出てくるので確定か、このパソコンからのUSBデバッグを常に許可するのいずれかを選択することでデバッグが可能となります。

BluetoothによるデバッグはPCとの接続が失われた時点で無効になるので、その時はターミナルのコマンドをもう一度実行してください。

これでテスト環境は完成です。
もちろんエミュレータを使ってテストすることも出来ます。

プロジェクトの作成

今回はありふれたアナログの時計を作成します。その後、プログラムの構造を追っていきましょう。
Watch Faceを作るためにAndroid StudioにてFile-New-New Projectにて新規プロジェクトを作成します。
Application nameとCompany Domainを入力します。


Target Android Devicesが表示されたら
Phone and Tabletにチェックし、Minimum SDKをAPI 18: Android4.3 (Jelly Bean)を選びます。
Android Wearに連携するにはAndroid4.3以上のモバイル端末が必要ですので、この指定を行うことでAndroid4.3未満のユーザーがGoogle PlayからWatch Faceをダウンロードしてしまうことを防ぐことが出来ます。

Wearにもチェックします。
WearのMinimum SDKはAPI 21:Android5.0(Lollipop)とします。
Watch FaceはAPI 21にて新規追加された機能ですが、Android Wearはモバイル端末に比べてバージョンアップの追跡が早いため、新しいAPIを使いやすい環境がそろっています。


Add an activity to Mobileでは
Add No Activityを選びます。


Add an Activity to Wearでは
Watch Faceを選びます。

Watch FaceのStyleはAnalogとDigitalから選ぶことが出来ます。
今回はAnalogを選びます。


これでWatch Faceができました。

Android Wearへのインストール

それではWatch FaceをAndroid Wearにインストールしてみます。
基本的な実行方法は普通のAndroidアプリと同じですが、複数のmoduleがあるため、実行するmoduleを選択します。

wearがAndroid Wearにインストールする側です。
mobileは実行するActivityが存在しないためエラーとなっていますが、今時点では気にしないでwearを選んでください。


あとはアプリを実行して、インストールする端末を選びます。
スマートフォンなどと混ざって毎回端末を選ぶのが面倒 という場合は
Use same device for future launchesにチェックを入れることで次回からインストールする端末を固定化することが出来ます。
この設定は端末との接続が失われるとリセットされます。


アプリを実行したらしばらく待つとWear側にNotificationが表示されるため、表示されたらWatch faceを新しく作ったWatch faceに変更します。
設定を変えていなければ My analogという名前になっているかと思います。

通常のActivityと違って自動で起動は行われないので注意してください。
アップデート時既にWatch faceの設定が行われているならば、アップデート後自動的に起動されます。
例外が発生した場合などはWatch faceが無効化されてしまうことがあるので、その場合はもう一度Watch faceを選択してください。

それではインストールされたWatch Faceの基本的な仕様を確認してみます。
まず、時、分、秒が白いラインで描かれていて、1秒毎に秒針が動いています。
しばらくすると薄暗いAmbient modeになり秒針が消えます。
アクティブな状態で画面をタップすると背景の色が変わります。

次回はWatch Faceがどのように動いているかを理解するためにソースコードを詳しく見て行きましょう。

Watch Faceを作る サンプルのコードを読み解く

$
0
0
Android WearのWatch Faceを作る 目次
前回は環境構築とチュートリアルに従って雛形を作成しました。
今回は前回作成したモジュールを元に、どのようにWatch Faceが動いているか読み解いていきます。

wearモジュールを開くとjavaファイルとしてMyWatchFaceが1つ作成されています。
この1つのファイルにWearアプリを作るために必要な殆どの内容が入っていて、初期にテンプレートとして作成されるファイルにしてはかなり大きなものになっています。
コメントも全文英語なのでちょっと分かりづらいですね。
それでは1つずつ分解してみていきましょう。

クラス構成

ファイルは1つですが、MyWatchFaceに加えて1つのstaticなinner classであるEngineHandlerとinner classであるEngineがあるため実際は3つのクラスから構成されています。

MyWatchFaceはWatchFaceの本体となるクラスです。
EngineはWatchFaceを作る上で最も重要なクラスでライフサイクルや描画などさまざまな処理を取り扱います。
EngineHandlerは時計で大切な一定時間後にEngineの処理を呼びます。

MyWatchFaceの処理

MyWatchFaceはその名の通り、WatchFaceの本体ですが、実際のところ殆どの処理はEngineに記載されており、Innerクラスを排除するとこれだけコンパクトになります。

/**
* Analog watch face with a ticking second hand. In ambient mode, the second hand isn't shown. On
* devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient mode.
*/
public class MyWatchFace extends CanvasWatchFaceService {
/**
* Update rate in milliseconds for interactive mode. We update once a second to advance the
* second hand.
*/
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);

/**
* Handler message id for updating the time periodically in interactive mode.
*/
private static final int MSG_UPDATE_TIME = 0;

@Override
public Engine onCreateEngine() {
return new Engine();
}
}

書き換え間隔の指定

書き換え間隔を秒で指定しています。ここでは1秒が指定されています。
実際に格納される値はミリ秒なので1000が設定されます。

private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);


ハンドラー送信元を特定するためのIDを指定ハンドラーの送信元を特定するためのIDを指定しています。
ここでは0が設定されていますが送信側と受信側が同じ値であればどんな値でも良いです。

private static final int MSG_UPDATE_TIME = 0;


Engineの作成

Engineのインスタンスを作成して返すことで、WatchFaceのライフサイクルでEngineを呼び出させることが出来ます。

@Override
public Engine onCreateEngine() {
return new Engine();
}


Engineの処理

EngineはWatchFaceの多くの処理を行います。
インスタンス変数として次のような値を持ちます。

final Handler mUpdateTimeHandler = new EngineHandler(this);
boolean mRegisteredTimeZoneReceiver = false;
Paint mBackgroundPaint;
Paint mHandPaint;
boolean mAmbient;
Time mTime;
final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mTime.clear(intent.getStringExtra("time-zone"));
mTime.setToNow();
}
};
int mTapCount;

mUpdateTimeHandlerには一定時間ごとのイベントを取り扱うためのEngineHandlerを持ちます。
mRegisteredTimeZoneReceiverはタイムゾーンの変更時のReceiverが登録されているかどうかを持ちます。
mBackgroundPaintは背景を描画するため、mHandPaintは針を描画するためのPaintです。
mAmbientは現在がAmbientモードになっているかどうかを持ちます。
mTimeは時間関係を取り扱うためのクラスです。Timeは扱える範囲が狭く2032年以降が取り扱えないので、本当ならこの実装はあまり良くない気がします。
余裕がある人はCalendarに書き換えてください。
mTimeZoneReceiverはタイムゾーンが変更された時のReceiverです。mTimeのタイムゾーンを設定して時間を置き換えています。このため、海外旅行などでタイムゾーンが変更された場合でも、自動的に新しいタイムゾーンに移行させることが出来ます。
mTapCountはタップされた回数を記録しています。これは背景をタップするたびに色を切り替えるために使っているだけで、WatchFaceの作り次第で普通は不要になると思います。
mLowBitAmbientはAmbient時に白黒2値をサポートするかどうかを格納します。

onCreate()の処理

Engineが作成された時に行う処理を記載します。
Watch Faceは実行時間がかなり長くなるのと、普段使用時の消費電力を極端に抑える必要が有ることから、1回で済む処理は全てここで記載してしまい、あとから行う処理を最小限にします。

WatchFaceのスタイルを設定します。
CardPeekModeは通知が来た時にどのようにWatchFace上に通知を見せるかを指定します。
BackgroundVisibilityは通知が来た時に背景を表示するかどうかを指定します。
ShowSystemUiTimeはシステムが提供する標準的なデジタルの時刻表示を行うかどうかを指定します。
WatchFaceだけでは時刻がわかりにくい場合などにもシステムの時刻が表示されます。
AcceptsTapEventsではユーザーがタップした時に処理を行うかどうかを指定します。

setWatchFaceStyle(new WatchFaceStyle.Builder(MyWatchFace.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.setAcceptsTapEvents(true)
.build());

描画を行うのに必要な準備を行います。
実際の描画は後で行いますが描画処理は何度も呼び出されるのでインスタンスの生成のような重たい処理は出来るだけonCreateのタイミングで行ってしまい、描画処理の負荷を下げます。

Resources resources = MyWatchFace.this.getResources();

mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(resources.getColor(R.color.background));

mHandPaint = new Paint();
mHandPaint.setColor(resources.getColor(R.color.analog_hands));
mHandPaint.setStrokeWidth(resources.getDimension(R.dimen.analog_hand_stroke));
mHandPaint.setAntiAlias(true);
mHandPaint.setStrokeCap(Paint.Cap.ROUND);

mTime = new Time();


onDestroy()の処理

onDestroy()では一定時間ごとに処理を行うmUpdateTimeHandlerのメッセージを削除して次回以降の処理が行われないようにします。

@Override
public void onDestroy() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
super.onDestroy();
}



onPropertiesChanged()の処理

onPropertiesChanged()はプロパティーが変更された時に呼び出されます。
ここではAmbient時に白黒2値をサポートしているかを判別し保存しています。

@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
}


onTimeTick()の処理

onTimeTick()は一定時間ごとに呼び出されます。invalidate()を呼んで画面の再描画を促します。
ただし、ここで呼び出されるのは最短でも1分に一度だけです。そのため、秒針などを表現したい場合は、mEngineHandlerを使って別に呼び出します。

@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}


onAmbientModeChanged()の処理

一定時間操作を行わないと、AmbientModeというバックライトが消えて色数が少ない省電力モードに移行します。
AmbientModeになった後は画面の色数を減らして、画面描画を最短で1分ごとに抑えます。
AmbientModeに入るときはinAmbientModeにtrueが、inAmbientModeから出るときはinAmbientModeにfalseが渡されます。

AmbientModeが切り替わった時は最新のAmbient状態をmAmbientに保存します。
つぎに、白黒2値のAmbientModeに対応している場合はPaintのAntiAliasもAmbient時はfalse、Ambientから出て通常状態になる時はOFFに書き換えます。
白黒2値のAmbientModeに対応していない場合はAntiAliasはtrueのままです。
これにより白黒2値に対応していてAmbientModeに入る時だけAntiAliasがfalseとなり、それ以外ではtrueとなります。

AntiAliasとは半透明の色を使うことで、線をなめらかに見せる機能ですが、その分余計に必要な処理が増えます。
白黒2値の場合半透明が使用できずせっかくAntiAliasを計算しても白か黒に丸められてしまうので、無駄な処理を行わないようにしているというわけです。

invalidate()を呼んで画面の再描画をリクエストして、次の1分まで待たずにAmbient状態に応じた描画を行います。
updateTimer()はAmbient状態と通常状態に応じて画面の描画間隔を切り替えるためにタイマーを設定します。(後述)


if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if (mLowBitAmbient) {
mHandPaint.setAntiAlias(!inAmbientMode);
}
invalidate();
}

// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}

onTapCommand()の処理

画面をタップした時はonTapCommand()が呼び出されます。
引数tapTypeはどのようにタップしたかについて、画面をタップし始めた時はTAP_TYPE_TOUCHユーザーがタップ操作を取りやめたり別のジェスチャーに切り替えた時はTAP_TYPE_TOUCHユーザーがタップ操作を完了した時はTAP_TYPE_TAPが渡されます。
int xとint yはタップした座標が渡されます。eventTimeはタップされた時間が渡されます。

このサンプルでは、タップが行われた場合、タップの回数をカウントし、タップの回数が偶数の時はR.color.backgroundを、奇数の時はR.color.bacground2を背景色に設定しています。
最後にタップ操作を即座に反映するためにinvalidate()を呼んで画面の再描画をリクエストします。

@Override
public void onTapCommand(int tapType, int x, int y, long eventTime) {
Resources resources = MyWatchFace.this.getResources();
switch (tapType) {
case TAP_TYPE_TOUCH:
// The user has started touching the screen.
break;
case TAP_TYPE_TOUCH:
// The user has started a different gesture or otherwise cancelled the tap.
break;
case TAP_TYPE_TAP:
// The user has completed the tap gesture.
mTapCount++;
mBackgroundPaint.setColor(resources.getColor(mTapCount % 2 == 0 ?
R.color.background : R.color.background2));
break;
}
invalidate();
}


onDarw()の処理

onDraw()内で実際の画面描画を行います。通常のAndroidアプリでもそうですが、Wearアプリでは特にこのonDraw()内の処理を短く終わらせる必要があります。

描画処理を行うための元となる時間を現在時刻にセットします。

mTime.setToNow();


前回までの描画をリセットするために背景色を塗りつぶします。
Ambientモードでは黒一緒、Ambientモード以外ではタップ数に応じてpaintに設定された色で塗りつぶします。
Ambientモードでは省電力のため画面を黒多めにすることが推奨されています。これは特に有機ELの液晶を持つ機種において消費電力の節約に役立ちます。

// Draw the background.
if (isInAmbientMode()) {
canvas.drawColor(Color.BLACK);
} else {
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mBackgroundPaint);
}


引数で渡されるboundsの半分を取得することで画面中央の座標を取得します。
実際には毎回の描画ごと座標を求めるのは非効率なのでインスタンス変数に格納して画面サイズが変わらないうちは再利用した方がいい気がします。

float centerX = bounds.width() / 2f;
float centerY = bounds.height() / 2f;


次に針の先部分を求めます。
これにはいくつかの方法があるのですが、ここでは三角関数を使って針の位置を求めています。
まず、針の角度を求めます。

通常、角度は0〜360度で求めますが、ここでは三角関数と相性がいい「ラジアン(rad)」と呼ばれる単位を使っています。
ラジアンは一周(360度)が2 x π(3.14) radに相当します。

円の外周は半径 x 2 x πなので、半径を1とすると弧の角度(rad)と弧長の長さが同じになります。

半径1mの正円では、角度1radの弧長は1m、角度2radの弧長は2mとなる。
円周の長さは2π m(いわゆる直径x3.14)になり、一周の角度は2π radとなる。

つまり角度がχ radの時、円弧の長さはχ×半径となります。

秒針の角度を求めます。
360°は2π radなので、秒(0〜59)を30で割ることで1分を0〜2(正確には59秒までしか無いので1.9666...)を求めてこの値にπをかけることで秒数に応じた0〜2π radを求めています。
これが秒針の角度(rad)になります。
秒針は1秒毎にカチカチと動くタイプなので秒だけを元に角度を求めています。

πの値はMath.PIで求めることが出来ます。 Math.PIは64bitの精度を持つdouble型で定義されていますが、そこまでの精度は必要ないのと処理量を抑えるために32bitの精度を持つfloat型に丸めて計算しています。

float secRot = mTime.second / 30f * (float) Math.PI;


同様に分針の角度も求めます。
こちらも、1分ごとに針が動くタイプなので分針だけを元に角度を求めています。

int minutes = mTime.minute;
float minRot = minutes / 30f * (float) Math.PI;


次に時針の角度も求めます。
時針は12時間で一周します。さらに1時間毎にカチカチと動くのではなく、分も考慮してなめらかに動くようにする必要もあります。
60(分)で1(時間)となるように分を60で割ります。この値を時間に足します。
これにより例えば4:30なら4.5に、2:15なら2.25になります。

この値を6で割ることで12時間で0〜2まで1分ごとに等分に変化する値を求めています。

float hrRot = ((mTime.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;

次に針の長さを求めます。
画面幅の半分から、秒針は20ひいた値、分針は40ひいた値、時針は80ひいた値としています。

float secLength = centerX - 20;
float minLength = centerX - 40;
float hrLength = centerX - 80;


秒針を描画します。ambientモードの時は1分に1回しか書き換えが行わないため、秒針を描画すると動かない秒針となってしまいます
そこで、Ambientモード以外(通常モードの時)だけ秒針を描画します。

針の先は三角関数を使って求めています。
時計における3時の方向を基準に sin(ラジアン角度)を渡すことで、針先のY座標を求めることが出来ます
同様にcos(ラジアン角度)を渡すことで、針先のX座標を求めることが出来ます。

ラジアン角度をχ、半径の長さを1とした場合、斜線と円の接する座標は次のように求めることが出来ます。


この時に求まる値は3時の方向を基準とした時計回りです。
※一般的に数学の説明では3時の方向を基準とした反時計回りで説明されます。
しかしながら、Androidをはじめとする多くのコンピューターの世界では下に行くほど数値が大きくなる上下が入れ替わった座標系を使っているため上下が反転されて時計回りとなります。

xをcos、yをsinとすると3時を基準とした時計回りとなる。


時計の針で欲しいのは12時を基準とした時計回りなので、ちょっと工夫して座標を入れ替えます。
まず、xとyを入れ替えます。
sinの結果をX、cosの結果をYとすることで、x=-yの斜線を対象とした座標となり6時を基準とした反時計回りになります。

xとyを入れ替えると6時を基準とした反時計回りとなる。



6時を基準とした反時計回りはちょうど12時を基準とした時計回りを上下反転した形になるので、Y座標に-1をかけて上下反転させます。

上下を反転させることで12時を基準とした時計回りとなる。



これで求まるのは、半径が1の場合の座標なので、線の長さ(secLength)を掛けて線の長さに応じた位置を求めます。
sin、cosで取得される座標は、円の中心が左上(X:0,Y:0)とした場合の場所なので、中心からの位置を足して、中心からの位置に移動してやります。

画面中心から、求まった線先の座標までの線をdrawLineで描くことで、秒針が描画できます。


if (!mAmbient) {
float secX = (float) Math.sin(secRot) * secLength;
float secY = (float) -Math.cos(secRot) * secLength;
canvas.drawLine(centerX, centerY, centerX + secX, centerY + secY, mHandPaint);
}


同様に分針と時針も描画します。
分針と時針はambientモードにかかわらず描画するため、if文の外で描画します。

float minX = (float) Math.sin(minRot) * minLength;
float minY = (float) -Math.cos(minRot) * minLength;
canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mHandPaint);

float hrX = (float) Math.sin(hrRot) * hrLength;
float hrY = (float) -Math.cos(hrRot) * hrLength;
canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHandPaint);


onVisibilityChanged()の処理


Watch Faceが表示されていない時に不要な描画を避けるため、
また、Watch Faceが表示された瞬間に最新の状態で画面を描画して更新を再開するために、onVisibilityChanged()で表示状態が変更された時の処理を行います。

画面が描画された時はタイムゾーンが変更された時の通知を受けるためのReceiverを登録します。
また、画面我猫されていない間にタイムゾーンが変更されている場合に備えて現在のタイムゾーンをmTimeに設定し現在時刻を取得します。

registerReceiver();

// Update time zone in case it changed while we weren't visible.
mTime.clear(TimeZone.getDefault().getID());
mTime.setToNow();


画面が非表示になる時にはタイムゾーンの切り替わりによるReceiverを解除します。

unregisterReceiver();


状態に応じて画面の更新を行い続けるかどうかを指定するためupdateTimer()を呼びます。

updateTimer();


registerReceiver()の処理

registerReceiver()ではタイムゾーンが変更された時に呼び出されるReceiverを登録しています。

private void registerReceiver() {
if (mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
MyWatchFace.this.registerReceiver(mTimeZoneReceiver, filter);
}


unregisterReceiver()の処理

registerReceiverで登録したReceiverを解除します。

private void unregisterReceiver() {
if (!mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = false;
MyWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
}


updateTimer()の処理

タイマーを解除した後、1秒毎に更新が必要な場合、タイマーを設定し直します。

private void updateTimer() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning()) {
mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
}


shouldTimerBeRunning()の処理

1秒ごとに更新が必要かどうかを返します。
1秒毎に更新が必要なのは画面が描画されておりambientModeではない時です。

private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}


handleUpdateTimeMessage()の処理

次の1秒で呼び出されるようにTimerを設定します。
この処理自体が毎回1秒毎に呼び出されます。最初にinvalidate()で画面描画処理のリクエストを行ったら、次の1秒後にまたこの処理が呼ばれるようにTimerを設定します。

現在時刻に対して描画間隔を足し、現在時刻から描画間隔のあまりをひいた時間に処理が呼び出されるようにしています。
これはなにをやっているかというと、タイマーは1秒毎に設定しても、きっかり1秒毎に呼び出されるとは限りません。
また、処理中も時間がかかるので、例えば00分00秒に処理が呼ばれた場合、
単純に現在時刻+1秒とすると、登録する時には既に00分00秒100ミリ秒になっているかもしれません。
そうすると、時間の処理は00分01秒100ミリ秒に呼び出されてしまいます。
このまま続けると、次第に時間がずれてきますし、処理に時間がかかった時だけ更新間隔が広がってしまい、ぎこちない動きになってしまいます。

そうなることを防ぐために、登録するとき秒の余分(100ミリ秒)削ることで、00分01秒000ミリ秒に次の処理が呼ばれるようにしています。


invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS
- (timeMs % INTERACTIVE_UPDATE_RATE_MS);
mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}



EngineHandlerの処理

EngineHandlerはEngine#handleUpdateTimeMessageで登録され、毎秒ごとにアラートで呼び出されます。
呼びだされたらEngine.handleUpdateTimeMessageを呼ぶことでEngine.handleUpdateTimeMessageとEngineHandlerがお互いを呼び合う形となり、毎秒ごとの処理を行います。


ただし、一秒とはいえ、未来時間で呼び出されるため、これが呼び出された時には既にWatchFaceは使われていない可能性があります。
そのようなときに、EngineHandlerがEngineの参照を保持していると、GC出来なくなってしまうので、EngineHandlerではEngineを弱参照で保持します。
メッセージを受け取ったら、弱参照のEngineが破棄されていないか確認し、破棄されていなければ、EngineのhandleUpdateTimeMessage()を呼び、描画と次の1秒への登録を行わせます。



private static class EngineHandler extends Handler {
private final WeakReference<MyWatchFace.Engine> mWeakReference;

public EngineHandler(MyWatchFace.Engine reference) {
mWeakReference = new WeakReference<>(reference);
}

@Override
public void handleMessage(Message msg) {
MyWatchFace.Engine engine = mWeakReference.get();
if (engine != null) {
switch (msg.what) {
case MSG_UPDATE_TIME:
engine.handleUpdateTimeMessage();
break;
}
}
}
}


標準サンプルではラインによる描画ですが、これでは個性的な時計を作りづらいです。
次回はこのサンプルを元に画像を使ったアナログ時計を作ってみましょう。

Watch Faceを作る 画像を使ってオリジナル時計を作る

$
0
0
Android WearのWatch Faceを作る 目次
前回はサンプルコードの仕組みを詳しく追っていきました。

今回はいよいよオリジナルの時計を作ります。自作した画像を使ってアナログ時計を作ってみましょう。
画像を使うことでより多彩な表現を行うことが出来るようになります。
一方で、画像を使う場合はいくつか考慮しないといけないことが増えます。

背景、時針、分針の画像を作る

背景と時針、分針は画像を使って書き出し、秒針はラインで描画することとします。
時針と分針は12時の方向を向いている状態で書き出します。

サンプルの画像も用意しました。サンプル全体のイメージとしてはこんな感じ

もちろん、自分ごのみの画像を用意してもらっても大丈夫です。
画像を用意するときは、背景、時針、分針をそれぞれ別で切り出し、時針と分針は背景が透明なPNG形式で出力するようにしてください。
画像サイズについては今回は512pxの正方形としています。

この画像をここでは便宜上「デザインした画像」と呼ぶことにします。
また、画像アプリ上での位置(px)をデザインした座標と呼びます。

画像ファイル一式をダウンロードする。
background.pngが背景画像
hour.pngが時針
minute.pngが分針
preview.pngがプレビュー用の画像です。
です。

画像ファイルを置くフォルダはwearプロジェクトのsrc/main/res/drawable-nodpiです。
drawableフォルダ内の画像は液晶の密度に合わせて適切な画像が選ばれ、適切に拡大縮小されますが、drawable-nodpiに保存された画像は液晶の密度にかかわらず、常に同じファイルがそのまま取得できます。
今回は画面サイズに合わせた拡大縮小処理を自前で実装するため、フレームワークの余計な処理を防ぐためにdrawable-nodpiに保存します。

Engineに必要なメンバ変数を追加する

デザイン時のサイズを設定する。

MyWatchFace.javaを開いてInnerクラスのEngine部分まで移動します。

static finalな定数にデザインした画像のサイズを設定します。
今回は画像データを512px x 512pxでデザインしたため512をセットしておきます。

private static final float DESIGNED_SIZE = 512f;


Bitmapをキャッシュする変数を作る。

Bitmapの読み込みや拡大縮小は処理負荷が大きいため、描画ごとに毎回行うのではなく、一度読み込んだ画像は変数に格納して再利用するようにします。
今回は背景、時針、分針が画像のため、それぞれのBitmap変数をEngineのインスタンス変数として定義します。

private Bitmap mBackground;
private Bitmap mHour;
private Bitmap mMinute;


サンプルでは背景の描画をmBackgroundPaintが針の描画をmHandPaintが行っていました。
今回はこの役割を変更して、画像データを描画するPaintとそれ以外のPaintとします。
わかりやすいように変数名を変更しましょう。
mBackgroundPaintをmBitmapPaintにmHandPaintをmDrawPaintにそれぞれ変更します。
変数名を一度に変更するときは変更したい変数にカーソルを合わせてShift+F6を使うと便利です。
新しい変数名に変更してENTERを押すと同じ変数が全て入れ替わります。

Paint mBitmapPaint;
Paint mDrawPaint;


画面サイズに応じた値も、毎回計算するのではなく、インスタンス変数に格納するようにします。記録するのは、画面幅、画面高さ、中央位置(縦、横)、秒針の長さ、中央穴の半径、画像を使う素材(今回は背景、時針、分針)は基準となる左上の座標(縦、横)、デザインした画像のサイズ(512px)に対する実画面の比率、画像を変形させるのに使用するMatrixです。
Matrixはimportが複数あるため、android.graphics.Matrix;をインポートします。

private Bitmap mBackground;
private Bitmap mHour;
private Bitmap mMinute;
private int mWidth;
private int mHeight;
private float mCenterX;
private float mCenterY;
private float mSecLength;
private float mHoleRadius;
private float mBackgroundTop;
private float mBackgroundLeft;
private float mHourLeft;
private float mHourTop;
private float mMinuteLeft;
private float mMinuteTop;
private float mScale;
private Matrix mMatrix;



onTapCommand()を修正する。

サンプルでは画面をタップすると背景色が変わる処理が入っていましたが、今回はタップイベントを特に設定しないため、消しておきます。

mTapCount++; // 削除
mBackgroundPaint.setColor(resources.getColor(mTapCount % 2 == 0 ? // 削除
R.color.background : R.color.background2)); // 削除


onCreate()を修正する。

mDrapPaintについて、setAntiAlias(true)にてギザギザが目立たないなめらかなラインを描いていますが、mBitmapPaintは画像を取り扱います。
画像ではアンチエイリアスは画像の端部分にしか適用されず、あまり効果がありません。

変形した画像をなめらかに描画するにはsetFilterBitmap(true)を指定します。mBitmapPaintの設定にsetFilterBitmap(true)を追加しましょう。

mBitmapPaint.setColor(resources.getColor(R.color.background));
mBitmapPaint.setFilterBitmap(true); // 追加

画像を回転させるために使用するMatrixを初期化する処理を追加します。

mMatrix = new Matrix();


onCraete()全体は次のようになります。

@Override
public void onCreate(SurfaceHolder holder) {
super.onCreate(holder);

setWatchFaceStyle(new WatchFaceStyle.Builder(MyWatchFace.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.setAcceptsTapEvents(true)
.build());

Resources resources = MyWatchFace.this.getResources();

mBitmapPaint = new Paint();
mBitmapPaint.setColor(resources.getColor(R.color.background));
mBitmapPaint.setFilterBitmap(true);
mDrawPaint = new Paint();
mDrawPaint.setColor(resources.getColor(R.color.analog_hands));
mDrawPaint.setStrokeWidth(resources.getDimension(R.dimen.analog_hand_stroke));
mDrawPaint.setAntiAlias(true);
mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
mTime = new Time();
mMatrix = new Matrix();
}

onAmbientModeChanged()を書き換える

AmbientModeに入った時にmDarwPaintのアンチエイリアスを指定しているところへ、mBitmapPaintのフィルターを設定する処理を追加します

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if (mLowBitAmbient) {
mDrawPaint.setAntiAlias(!inAmbientMode);
mBitmapPaint.setFilterBitmap(!inAmbientMode); // 追加
}
invalidate();
}

// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}



onDraw()を書き換える

画面サイズが切り替わった時の処理

サンプルのonDraw()では毎回中央位置を計算していましたが、実際の処理では解像度が変わることは殆どありません。
そこで解像度が替わった時だけ中央位置を計算し、合わせて画像の拡大縮小や、基準点の算出、秒針の長さの設定などを行って、それ以外の時は変数に格納された値を使用するようにします。
前回計算した画面幅、高さと現在の幅、高さを比較することで画面の拡大縮小が発生したかどうかを確認できます。


@Override
public void onDraw(Canvas canvas, Rect bounds) {
mTime.setToNow();
// 以下追加
// 画面サイズが変わったらtrue
boolean isSizeChanged
= canvas.getWidth() != mWidth || canvas.getHeight() != mHeight;
if(isSizeChanged){
// この中に処理を追加していきます
}

画面幅と高さを記憶し、中央位置はサンプル同様に縦横を半分にして計算します。

mWidth = canvas.getWidth();
mHeight = canvas.getHeight();
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;

実サイズとの比率は実サイズ(長編)に対してデザインした画像のピクセル数512px(DESIGNED_SIZE)を割ることで求めています。

int longSize = Math.max(canvas.getWidth(),canvas.getHeight());
mScale = longSize / DESIGNED_SIZE;

背景画像の座標は、画面サイズと画面長辺の差を2で割ることで画像を中央に表示されるようにしています。

mBackgroundLeft= (mWidth - longSize) / 2f;
mBackgroundTop= (mHeight- longSize) / 2f;

例えば画面サイズが縦400x幅300のウォッチなら次のようになります。
left=300-400/2 = -50
top=400-400/2 = 0

画像は正方形で用意して有り、画面の長辺に合わせてリサイズするため400x400の画像です。
これにより画像が画面中央に表示されます。


サンプルでは秒針の長さを単純に画面サイズから引き算していましたが、画像と組み合わせるため、画像の拡大縮小に応じてサイズを変える必要があります。デザインした秒針のサイズ(今回は200pxとしました)にmScaleを掛けることで秒針の長さを求めます。

mSecLength = (int)(200f * mScale);

中央穴の半径はデザインしたサイズにmScaleをかけることで求めます。

mHoleRadius = 12f * mScale;

時針と分針の縦横は画面に対する針の左上の座標を指定します。

デザインした座標にmScaleを掛け、それにmBackgroundLeftあるいはmBackgroundTopを足すことで求まります。


mHourLeft = 244f * mScale + mBackgroundLeft;
mHourTop = 80f * mScale + mBackgroundTop;
mMinuteLeft = 242f * mScale + mBackgroundLeft;
mMinuteTop = 54f * mScale + mBackgroundTop;


画面サイズが切り替わった時の処理全体は次のとおりです。

boolean isSizeChanged = canvas.getWidth() != mWidth || canvas.getHeight() != mHeight;
if (isSizeChanged) {
mWidth = canvas.getWidth();
mHeight = canvas.getHeight();
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;
int longSize = Math.max(canvas.getWidth(), canvas.getHeight());
mScale = longSize / DESIGNED_SIZE;
mBackgroundLeft = (mWidth - longSize) / 2f;
mBackgroundTop = (mHeight - longSize) / 2f;

mSecLength = (int) (200f * mScale);

// 中央穴の位置
mHoleRadius = mScale * 12f;

// 縦横の取得
mHourLeft = 244f * mScale + mBackgroundLeft;
mHourTop = 80f * mScale + mBackgroundTop;
mMinuteLeft = 242f * mScale + mBackgroundLeft;
mMinuteTop = 54f * mScale + mBackgroundTop;


画面の取得

画面サイズが変わった時は画像を取得して画面サイズに応じた拡大・縮小処理を行う必要があります。
画像サイズが変わった時に加えて画像が何らかの理由で取得できていなかったり、破棄されている場合も処理を行うようにします。

画像の取得と拡大縮小は背景と、長針、短針の全てで同じ方法を使いますし、今後画像が増えた時のためにメソッド化しておきましょう。

Engineクラスに次のメソッドを追加します。
Resourcesと画像のidを指定してリサイズされた画像を作成するメソッドを作ります。
最初にIDにもとづいて画像を取得したら、Bitmap.createScaledBitmap()により必要なサイズに画面を縮小・拡大したBitmapを返します。


private Bitmap createSaledBitmap(Resources resources, int id){
Bitmap original=((BitmapDrawable)(resources.getDrawable(id))).getBitmap();
Bitmap scaled = Bitmap.createScaledBitmap(original, (int)(original.getWidth() * mScale), (int)(original.getHeight() * mScale), true );
original.recycle();
return scaled;
}


再度onDraw()に戻ります。
画面サイズが切り替わった時の処理の後に画面サイズが変わった時に加えて画像が取得できていない時、画像が破棄された時にも、先ほど追加したメソッドを呼び出して新しいサイズに適合した画像を取得する処理を追加します。。

if(isSizeChanged
|| mBackground == null || mBackground.isRecycled()
|| mHour== null || mHour.isRecycled()
|| mMinute == null || mMinute.isRecycled()){
Resources resources = getResources();
mBackground = createSaledBitmap(resources, R.drawable.background);
mHour = createSaledBitmap(resources, R.drawable.hour);
mMinute = createSaledBitmap(resources, R.drawable.minute);
}


Draw the background部分の書き換え。

サンプルではAmbientModeの時は黒、通常時は背景色を描画していましたが、今回ではAmbientMode時はこれまでどおり黒、通常時はBitmapを中央揃えで描画します。

if (isInAmbientMode()) {
canvas.drawColor(Color.BLACK);
} else {
canvas.drawBitmap(mBackground,mBaseX , mBaseY, mBackgroundPaint);
}


中央位置の計算を毎回やっていた処理は削除します。

float centerX = bounds.width() / 2f; // 削除する
float centerY = bounds.height() / 2f; // 削除する


針の長さをサイズから引き算で求めていた処理も不要です。

float secLength = centerX - 20; // 削除する
float minLength = centerX - 40; // 削除する
float hrLength = centerX - 80; // 削除する


針の描画

サンプルではここから針の描画処理となっていますが、
サンプルでは秒針、分針、時針の順に描画されています。
描画は後から処理されたほうが上書きされるのでこのままでは秒針が一番後ろ、時針が一番前になってしまいます。
そこで、描画順を時針、分針、秒針の順に描画します。

まずは分針と時針の処理を削除します。

float minX = (float) Math.sin(minRot) * minLength; // 削除する
float minY = (float) -Math.cos(minRot) * minLength; // 削除する
canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mHandPaint); // 削除する

float hrX = (float) Math.sin(hrRot) * hrLength; // 削除する
float hrY = (float) -Math.cos(hrRot) * hrLength; // 削除する
canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHandPaint); // 削除する


次に時針と分針の描画を加えますが、今回は画像を使用するためdrawLineが使えません。
代わりに画像を回転させるためにMatrixを使用します。Matrixを使用することでキャンバスを好きなように変形させることが出来ます。

時針の角度を求めます。
前回はradで求めましたがMatrixは360度で指定します。時間の場合は1〜12時間を360度に分割するため時に30を掛けます。
60分=1時間となるように分も加えます。

float hourRotate= (mTime.hour + mTime.minute / 60f) * 30;


針の描画は最初に針の位置を移動して、針を回転させます。順番を逆にすると針の回転位置がずれてしまうので注意してください。
Matrixでは最初にsetから始まるメソッドで変形を指定し、次にpostから始まるメソッドで変形を加えていきます。
setから始まるメソッドを実行するとそれまでに設定されている変形が全てリセットされるため必ず最初にsetから行います。

setTranslate()により座標を平行移動させることが出来ます。
引数は横方向の移動距離、縦方向の移動距離です。


mMatrix.setTranslate(mHourLeft, mHourTop);


postはこれまで指定されている変形に新たな変形を加えます。
postと別にpreというのもありますが、これは変形を適用する順番をこれまでの変形よりも前に割り込ませたい場合に行います。あまり直感的とは言えないので特に理由がない場合はpostを使ったほうが読みやすいコードになります。

postRotate()により画像を回転させることが出来ます。引数は角度(°)、回転させる中央位置の横方向、中央位置の縦方向です。
位置を指定しなかった場合は画像の中央が指定されたものとみなされます。

今回は画面の真ん中を基準に回転するので既に取得済みのmCenterXと,mCenterYを指定します。

mMatrix.postRotate(hourRotate, mCenterX, mCenterY);


時針を描画します。
drawBitmapの第二引数にmMatrixを指定することで変形を反映できます。

canvas.drawBitmap(mHour, mMatrix, mBitmapPaint);


同様に分の角度も求めて分針を描画します。
分針の角度は分×6で求めることが出来ます。
秒を加える事でよりなめらかな分針にすることも出来ます。

float minuteRotate= (mTime.minute + mTime.second/ 60f) * 6;
mMatrix.setTranslate(mMinuteLeft, mMinuteTop);
mMatrix.postRotate(minuteRotate, mCenterX, mCenterY);
canvas.drawBitmap(mMinute, mMatrix, mBitmapPaint);


秒針を描画します。
秒針はdrawLineで描くので基本的にはこれまで通りです。一部変数をインスタンス変数に移動しているのでそれに合わせて調整します。

if (!mAmbient) {
float secRot = mTime.second / 30f * (float) Math.PI;
float secX = (float) Math.sin(secRot) * mSecLength;
float secY = (float) -Math.cos(secRot) * mSecLength;
canvas.drawLine(mCenterX, mCenterY, mCenterX + secX, mCenterY + secY, mDrawPaint);
}


最後に中央穴部分を描画します。

canvas.drawCircle(mCenterX, mCenterY, mHoleRadius, mDrawPaint);


onDraw()全体は次のようになります。


@Override
public void onDraw(Canvas canvas, Rect bounds) {
mTime.setToNow();
boolean isSizeChanged = canvas.getWidth() != mWidth || canvas.getHeight() != mHeight;
if (isSizeChanged) {
mWidth = canvas.getWidth();
mHeight = canvas.getHeight();
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;
int longSize = Math.max(canvas.getWidth(), canvas.getHeight());
mScale = longSize / DESIGNED_SIZE;
mBackgroundLeft = (mWidth - longSize) / 2f;
mBackgroundTop = (mHeight - longSize) / 2f;

mSecLength = (int) (200f * mScale);

// 中央穴の半径
mHoleRadius = mScale * 12f;

// 縦横の取得
mHourLeft = 244f * mScale + mBackgroundLeft;
mHourTop= 80f * mScale + mBackgroundTop;
mMinuteLeft = 242f * mScale + mBackgroundLeft;
mMinuteTop = 54f * mScale + mBackgroundTop;

}

if (isSizeChanged
|| mBackground == null || mBackground.isRecycled()
|| mHour == null || mHour.isRecycled()
|| mMinute == null || mMinute.isRecycled()) {
Resources resources = getResources();
mBackground = createScaledBitmap(resources, R.drawable.background);
mHour = createScaledBitmap(resources, R.drawable.hour);
mMinute = createScaledBitmap(resources, R.drawable.minute);
}

// Draw the background.
if (isInAmbientMode()) {
canvas.drawColor(Color.BLACK);
} else {
canvas.drawBitmap(mBackground, mBackgroundLeft, mBackgroundTop, mBitmapPaint);
}
float hourRotate= (mTime.hour + mTime.minute / 60f) * 30;
mMatrix.setTranslate(mHourLeft, mHourTop);
mMatrix.postRotate(hourRotate, mCenterX, mCenterY);
canvas.drawBitmap(mHour, mMatrix, mBitmapPaint);

float minuteRotate= (mTime.minute + mTime.second/ 60f) * 6;
mMatrix.setTranslate(mMinuteLeft, mMinuteTop);
mMatrix.postRotate(minuteRotate, mCenterX, mCenterY);
canvas.drawBitmap(mMinute, mMatrix, mBitmapPaint);

if (!mAmbient) {
float secRot = mTime.second / 30f * (float) Math.PI;
float secX = (float) Math.sin(secRot) * mSecLength;
float secY = (float) -Math.cos(secRot) * mSecLength;
canvas.drawLine(mCenterX, mCenterY, mCenterX + secX, mCenterY + secY, mDrawPaint);
}
canvas.drawCircle(mCenterX, mCenterY, mHoleRadius, mDrawPaint);

}


XML等を調整する

秒針を補足する

時針や分針に対して秒針が太すぎる感じがします。そこで秒針の太さを変えてみます。
onCreate()を見ると秒針の太さがXMLで指定されていることがわかります。

mDrawPaint.setStrokeWidth(resources.getDimension(R.dimen.analog_hand_stroke));

dimens.xmlを開くとanalog_hand_strokeが3dpで指定されているので、これを1dpに変更します。

これで秒針が細くなりました。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="analog_hand_stroke">1dp</dimen>
</resources>


プレビューを変更する

WatchFaceを変更するときのプレビューはdrawable-nodpiフォルダ内にあるanalog_preview.pngに格納されています。
このファイルを上書きすることでプレビューを変更できます。

別のファイル名に指定したい場合はAndroidManifest.xmlを指定します。

<!-- 四角い時計のプレビュー -->
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_analog" />
<!-- 丸い時計のプレビュー -->
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_analog" />



時計の名前を変更する

時計の名前を変更したい場合はStrings.xmlのmy_analog_name要素を書き換えます。

<resources>
<string name="app_name">My Application</string>
<string name="my_analog_name">My Analog</string>
</resources>


次回はより滑らかなアニメーションを行うためにミリ秒を取り扱う方法とユーザーがカスタマイズ可能な設定画面の作り方を紹介します。

Watch Faceを作る 設定画面の作成

$
0
0
Android WearのWatch Faceを作る 目次
今回は針の動きを滑らかにして設定画面の作り方を見ていきます。

Timeの置き換え

非推奨のTimeをCalendarに置き換えます。
Engineクラスのメンバ変数宣言部にあるTime mTimeをCalendar mCalendarに置き換えます。

Time mTime; // 削除
Calendar mCalendar; // 追加


mTime関連の場所がエラーになるので順番にCalendarの処理に置き換えていきます。
mTimeZoneReceiverのonReceive()を書き換えます。

mTime.clear(intent.getStringExtra("time-zone")); // 削除
mTime.setToNow(); // 削除
mCalendar.setTimeZone(TimeZone.getTimeZone(intent.getStringExtra("time-zone"))); //追加


onCreate()のインスタンス生成部分を置き換えます。

mTime = new Time(); // 削除
mCalendar = Calendar.getInstance(); //追加


onDraw()の時間設定部分を書き換えます。

mTime.setToNow(); // 削除
mCalendar.setTimeInMillis(System.currentTimeMillis()); // 追加


onDraw()で時針の角度を求めている部分を修正します。

float hourRotate = (mTime.hour + mTime.minute / 60f) * 30; // 削除
float hourRotate
= (mCalendar.get(Calendar.HOUR)
+ mCalendar.get(Calendar.MINUTE) / 60f) * 30; // 追加

分針の角度を求めている部分を修正します。

float minuteRotate= (mTime.minute + mTime.second/ 60f) * 6; // 削除
float minuteRotate = (mCalendar.get(Calendar.MINUTE)
+ mCalendar.get(Calendar.SECOND) / 60f) * 6; // 追加

秒針の角度を求めている部分を修正します。

float secRot = mTime.second / 30f * (float) Math.PI; // 削除
float secRot = mCalendar.get(Calendar.SECOND) / 30f * (float) Math.PI; // 追加

onVisibilityChanged()でタイムゾーンの値を再取得している処理を修正します。

mTime.clear(TimeZone.getDefault().getID()); // 削除
mTime.setToNow(); // 削除
mCalendar.setTimeZone(TimeZone.getDefault()); // 追加


秒針を滑らかにする

まずは秒針がスーッとなめらかに動くような処理を作ってみます。
そのためには描画の更新間隔を現在の1秒からもっと細かくする必要があります。
1秒間にどれだけ画像を更新するかをFPSと言います。FPSが多いほど滑らかになりますがCPUの負荷が増えバッテリーに負担がかかります。
サンプルは1秒間に1度だけ表示していたので1FPSです。なめらかなアニメーションを実現するには一般的には30FPSや60FPSがよく使われます。

今回は30FPSで更新を行います。
画面の更新頻度はINTERACTIVE_UPDATE_RATE_MSで行っていました。ここでは次の更新までどれだけの時間を待機するかを設定します。
サンプルは1秒間に1回処理を行うので、待機時間は1秒でした。
今回は1秒間に60回処理を行うので待機時間を1/30秒とします。

private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1) / 30;

これだけではなめらかな針の動きになりません。
画面の描画が1/60秒ごとになっても、針の動きが秒までしか考慮していないので無駄にCPUを浪費しているだけになります。
そこで秒針の角度をミリ秒を想定した動きに変えます。

onDraw()で秒針の角度(secRot)を求めている処理にミリ秒を追加します

secRot = (mCalendar.get(Calendar.SECOND)
+ mCalendar.get(Calendar.MILLISECOND) / 1000f) / 30f * (float) Math.PI;


プログラムを実行すると秒針がなめらかに動くのがわかります。

秒針がなめらかに動くのも良いのですが人によっては秒針は1秒毎カチカチと動くほうが好みという人もいるかと思います。
それについても、よりリアル感を出すために、こった動きをするようにしてみましょう。
針が一瞬で動くのではなく、0.8秒間は止まって残り0.2秒かけて次の秒へ移動するアニメーションを追加します。

なめらかに動くロジックも後で使うのでコメントアウトしておきます。

/*secRot = (mCalendar.get(Calendar.SECOND)
+ mCalendar.get(Calendar.MILLISECOND) / 1000f) / 30f * (float) Math.PI;*/

0.8秒間、すなわちミリ秒が0〜800までは秒針の位置に留まります。

int milliSecond = mCalendar.get(Calendar.MILLISECOND);
if(milliSecond <= 800){
secRot = mCalendar.get(Calendar.SECOND) / 30f * (float) Math.PI;
secRot = (mCalendar.get(Calendar.SECOND) + shift * shift) / 30f * (float) Math.PI;
}

ミリ秒が800を超えたら、ミリ秒から800を引いた値を200で割った値(すなわち0〜1)を求めて、その値を2乗した値を秒に足します。
2乗しているのは針の速度が直線的にならないようにするためです。
例えば2乗しなかった場合、ミリ秒が820の時は
(820-800)/200=0.1
ミリ秒が840の時は
(840-800)/200=0.2
ミリ秒が860の時は
(860-800)/200=0.3
という感じで20ミリ秒ごとにきっちり0.1増加していきます。

この値を2乗すると
ミリ秒が820の時は
0.1*0.1=0.01
で、最初の20ミリ秒は0.01しか増加しません。
ミリ秒が840の時は
0.2*0.2=0.04
で、次の20ミリ秒は0.03増加します。
次の20ミリ秒では0.05増加するという具合に、時間が経つほど加速する動きになり、針をより自然に動かすことが出来ます。


設定画面を作る

次に設定画面でなめらかな動きにするか、1秒毎に動くようにするか切り替えられるようにしましょう。

Watch FaceはWear上とスマートフォン上の2つの方法でカスタマイズ可能です。
これらはどちらか片方だけ作ることも出来ますし両方作る事もできます。
今回はWear上の設定画面を作ってみます。

Configクラスの作成Android Wearは通常のAndroidと同様に様々な方法でデータを保管することが出来ます。
Wearableの設定を使う場合はData Itemsを使うのが便利です。
Data itemsはウェアラブル端末向けに最適化されており、スマートフォンとウェアラブルの同期を自動的に行なってくれるなど便利な機能がたくさんあります。

設定値を保存するためのConfigクラスを作ります。INNERクラスではなく新しいJavaファイルで作っておきます。

public class Config {

}


定数の設定

データを保存するパスを指定します。

private static final String PATH = "/config";


設定したい値はDataMapというクラスにkey-value形式で保存します。
今回は針の動きを表すキーを設定します。

private static final String KEY_SMOOTH_MOVE = "SMOOTH_MOVE";


リスナーの実装

データの更新を取り扱うために DataApi.DataListener、GoogleApiClient.ConnectionCallbacks、GoogleApiClient.OnConnectionFailedListener を実装します。

public class Config implements DataApi.DataListener,
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener{
}

必要なメソッドを実装します。

public class Config implements DataApi.DataListener,
private static final String PATH = "/config";
private static final String KEY_SMOOTH_MOVE = "SMOOTH_MOVE";

GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener{
@Override
public void onConnected(Bundle bundle) {

}

@Override
public void onConnectionSuspended(int i) {

}

@Override
public void onDataChanged(DataEventBuffer dataEventBuffer) {

}

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {

}
}


設定が更新された時にConfigを呼び出した画面側もコールバックを受け取れるように、独自のコールバックを作成します。

private final WeakReference<OnConfigChangedListener> mConfigChangedListenerWeakReference;
public interface OnConfigChangedListener {
void onConfigChanged(Config config);
}


GoogleApiClientの設定

データの同期を取り扱うGoogleApiClientをメンバ変数として宣言します。

private GoogleApiClient mGoogleApiClient;


外部からデータの同期を開始、終了出来るようにメソッドを作ります。
接続時はデータの更新を受け取れるようにリスナーも登録します。Config自身がDataListenerを実装しているためthisを指定します。
切断時はリスナーを解除して切断します。

public void connect() {
mGoogleApiClient.connect();
Wearable.DataApi.addListener(mGoogleApiClient, this);
}

public void disconnect() {
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
Wearable.DataApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect();
}
}



コンストラクタの作成

GoogleApiClientの初期化はConfigクラスのコンストラクターで行います。
GoogleApiClient.Builder()は引数としてContextを渡します。Contextは実行しているActivityなどを使うのでコンストラクターの引数としてContextを取るようにしましょう。

コンストラクターの第二引数として先ほど追加したコールバックも受け取ることにします。

public Config(Context context, OnConfigChangedListener reference) {
if (reference == null) {
mConfigChangedListenerWeakReference = null;
} else {
mConfigChangedListenerWeakReference = new WeakReference<>(reference);
}
mGoogleApiClient = new GoogleApiClient.Builder(context)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Wearable.API)
.build();
}



設定値を保存する領域を作ります。秒針が滑らかに動くか1秒毎動くかの2択ですのでboolean型としましょう。
今後もっと多彩な針の動きを用意する予定でしたらIntegerに設定するという手もあります。

private boolean mIsSmooth;


GetterとSetterも作りましょう。
Setter内ではDataItemを使用して同期も行います。

同期を行うためにPutDataMapRequest.create(PATH);を呼んでDataMapRequestを作成します。
DataMapRequestのインスタンスに対してgetDataMap()を呼び値を同期するためのDataMapを取得します。
取得したDataMapにputBoolean(キー,値)を設定することでキーに紐づく値を保存できます。
int型で保存したいときはputInt(キー,値)と設定してください。

Wearable.DataApi.putDataItem()を呼ぶことでDataMapがDataItemとして保存・同期されます。

public boolean isSmooth() {
return mIsSmooth;
}

public void setIsSmooth(boolean isSmooth) {
mIsSmooth = isSmooth;
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH);
DataMap dataMap = putDataMapRequest.getDataMap();
dataMap.putBoolean(KEY_SMOOTH_MOVE, mIsSmooth);
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataMapRequest.asPutDataRequest());
}


設定を取得する。

現在の設定値を取得するためにonConnected()に値を取得する処理を追加します。
現在の値を取得するにはstaticメソッドであるWearable.DataApi.getDataItems()に引数としてGoogleApiClientを渡して、setResultCallback()で値が取れた時のコールバック処理を設定します。

@Override
public void onConnected(Bundle bundle) {
Wearable.DataApi.getDataItems(mGoogleApiClient)
.setResultCallback(new ResultCallback<DataItemBuffer>() {
@Override
public void onResult(DataItemBuffer dataItems) {

}
});
}

DataItemBufferは複数の値を持っていることがあるため、forで各値を確認します。取得したDataItemのパスがデータを保存するパスと一致している場合(今回は他に保存している処理がないため一致するはずですが)DataItemからDataMapを取り出し、KEYを指定して設定値を取得します。最後にdataItemsをrelease()しましょう。

@Override
public void onConnected(Bundle bundle) {
Wearable.DataApi.getDataItems(mGoogleApiClient)
.setResultCallback(new ResultCallback<DataItemBuffer>() {
@Override
public void onResult(DataItemBuffer dataItems) {
for (DataItem dataItem : dataItems) {
if (dataItem.getUri().getPath().equals(PATH)) {
DataMap dataMap = DataMap.fromByteArray(dataItem.getData());
mIsSmooth = dataMap.getBoolean(KEY_SMOOTH_MOVE, true);

if (mConfigChangedListenerWeakReference != null) {
OnConfigChangedListener listener = mConfigChangedListenerWeakReference.get();
if (listener != null) {
listener.onConfigChanged(Config.this);
}
}
}
}
dataItems.release();
}
});
}



設定値が変わった場合の処理も追加します。
設定値が変わるとonDataChangedが呼ばれます。引数としてDataEventBufferが渡されます。
そこでDataEventBufferからforで各イベントを処理します。
DataEventのgetType()がTYPE_CHANGEDの時だけ処理を行います。

これとは別に値が削除された時にTYPE_DELETEDが呼ばれる可能性もありますが、今回は割愛します。
getType()がTYPE_CHANGEDだった場合、getDataItem()でDataItemを求め、そのパスがこのアプリで設定したPATHと一致する場合に、変更されたデータを格納します。
画面からコールバックが設定されていればコールバックを呼びます。

@Override
public void onDataChanged(DataEventBuffer dataEventBuffer) {
for (DataEvent event : dataEventBuffer) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
DataItem item = event.getDataItem();
if (item.getUri().getPath().equals(PATH)) {
DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
mIsSmooth = dataMap.getBoolean(KEY_SMOOTH_MOVE);
if (mConfigChangedListenerWeakReference != null) {
OnConfigChangedListener listener = mConfigChangedListenerWeakReference.get();
if (listener != null) {
listener.onConfigChanged(Config.this);
}
}
}
}
}
}


Config全体は次のとおりです。
かなり大きくなっていますが、これを用意しておくことで設定の実装が簡単になります。

package org.firespeed.myapplication;

import android.content.Context;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataItem;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.Wearable;

import java.lang.ref.WeakReference;

public class Config implements DataApi.DataListener,
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final String PATH = "/config";
private static final String KEY_SMOOTH_MOVE = "SMOOTH_MOVE";

private GoogleApiClient mGoogleApiClient;
private boolean mIsSmooth;
public boolean isSmooth() {
return mIsSmooth;
}
public void setIsSmooth(boolean isSmooth) {
mIsSmooth = isSmooth;
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH);
DataMap dataMap = putDataMapRequest.getDataMap();
dataMap.putBoolean(KEY_SMOOTH_MOVE, mIsSmooth);
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataMapRequest.asPutDataRequest());
}

private final WeakReference<OnConfigChangedListener> mConfigChangedListenerWeakReference;

public Config(Context context, OnConfigChangedListener reference) {
if (reference == null) {
mConfigChangedListenerWeakReference = null;
} else {
mConfigChangedListenerWeakReference = new WeakReference<>(reference);
}
mGoogleApiClient = new GoogleApiClient.Builder(context)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Wearable.API)
.build();
}

public void connect() {
mGoogleApiClient.connect();
Wearable.DataApi.addListener(mGoogleApiClient, this);
}

public void disconnect() {
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
Wearable.DataApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect();
}
}

@Override
public void onConnected(Bundle bundle) {
Wearable.DataApi.getDataItems(mGoogleApiClient)
.setResultCallback(new ResultCallback<DataItemBuffer>() {
@Override
public void onResult(DataItemBuffer dataItems) {
for (DataItem dataItem : dataItems) {
if (dataItem.getUri().getPath().equals(PATH)) {
DataMap dataMap = DataMap.fromByteArray(dataItem.getData());
mIsSmooth = dataMap.getBoolean(KEY_SMOOTH_MOVE, true);
if (mConfigChangedListenerWeakReference != null) {
OnConfigChangedListener listener = mConfigChangedListenerWeakReference.get();
if (listener != null) {
listener.onConfigChanged(Config.this);
}
}
}
}
dataItems.release();
}
});
}

@Override
public void onConnectionSuspended(int i) {

}

@Override
public void onDataChanged(DataEventBuffer dataEventBuffer) {
for (DataEvent event : dataEventBuffer) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
DataItem item = event.getDataItem();
if (item.getUri().getPath().equals(PATH)) {
DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
mIsSmooth = dataMap.getBoolean(KEY_SMOOTH_MOVE);
if (mConfigChangedListenerWeakReference != null) {
OnConfigChangedListener listener = mConfigChangedListenerWeakReference.get();
if (listener != null) {
listener.onConfigChanged(Config.this);
}
}
}
}
}
}

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {

}

public interface OnConfigChangedListener {
void onConfigChanged(Config config);
}
}

設定に応じてWatch Faceの動きを変える

それではConfigの値を元にWatch Faceの動きを変えてみましょう。
そのためにはまずMyWatchFaceを開いて、Configを適切に取得する処理を追加する必要があります。
設定の更新を確認する必要があるのはonCraete()でWatch Faceが生成されてonDestroy()で破棄されるまでです。
画面が非表示の時は確認する必要がありません。

今回は秒針に関する処理なのでAmbientModeの時も取得する必要はありません。
時針のデザインを変えるなど、AmbientMode時でも描画される項目を変更する場合はAmbientMode時も設定の変更を監視し続けてください。

Configの作成と破棄

MyWatchFace.Engineのメンバ変数としてConfig mConfigを作ります。

private Config mConfig;

onCreate()でmConfigを作成し接続します。
コンストラクタの引数はContextとリスナーです。
今回はデータの更新を確認しなくても高頻度でConfigの値を参照することになるのでリスナーは登録しません。

mConfig = new Config(MyWatchFace.this, null);
mConfig.connect();


onDestroy()でmConfigを破棄します。

@Override
public void onDestroy() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
mConfig.disconnect();
mConfig = null;
super.onDestroy();
}


onAmbientModeChanged()でAmbientModeになった時に設定の確認を停止、AmbientModeから外れた時に再開します。

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if (mLowBitAmbient) {
mDrawPaint.setAntiAlias(!inAmbientMode);
mBitmapPaint.setFilterBitmap(!inAmbientMode);
}
mConfig.connect();
invalidate();
}else{
mConfig.disconnect();
}

// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}


onVisibilityChanged()で非表示になった時に設定の確認を停止、再表示された時に設定の確認を再開します。

@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);

if (visible) {
registerReceiver();

// Update time zone in case it changed while we weren't visible.
mCalendar.setTimeZone(TimeZone.getDefault());
mConfig.connect(); // 追加
} else {
unregisterReceiver();
mConfig.disconnect(); // 追加
}

// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}


Configの値を反映する

onDraw()で針の動きを設定している部分をmConfigの設定値に従って書き換えます。

if (!mAmbient) {
float secRot;
if(mConfig.isSmooth()) { // Configの値を取得する。
secRot = (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f) / 30f * (float) Math.PI;
}else{
int milliSecond = mCalendar.get(Calendar.MILLISECOND);
if(milliSecond <= 800){
secRot = mCalendar.get(Calendar.SECOND) / 30f * (float) Math.PI;
}else {
float shift = (mCalendar.get(Calendar.MILLISECOND) - 800) / 200f;

secRot = (mCalendar.get(Calendar.SECOND) + shift * shift) / 30f * (float) Math.PI;
}
}
float secX = (float) Math.sin(secRot) * mSecLength;
float secY = (float) -Math.cos(secRot) * mSecLength;
canvas.drawLine(mCenterX, mCenterY, mCenterX + secX, mCenterY + secY, mDrawPaint);
}
canvas.drawCircle(mCenterX, mCenterY, mHoleRadius, mDrawPaint);

これでConfigの内容を監視して針の動きを変える処理は出来ました。
しかし、これだけでは、Configの内容を書き換える処理がないため実際には設定を変えることが出来ません。
そこでまずは、タップ時に設定を書き換える処理を追加してみましょう。
前回無効にしたonTapCommand()を書き換えてタップすると針の動きが変わることを確認してください。
この時、この値は永続化されるため、他のWatch Faceに変えて、戻ってきた時も前回の値が保存されます。

@Override
public void onTapCommand(int tapType, int x, int y, long eventTime) {
Resources resources = MyWatchFace.this.getResources();
switch (tapType) {
case TAP_TYPE_TOUCH:
// The user has started touching the screen.
break;
case TAP_TYPE_TOUCH_CANCEL:
// The user has started a different gesture or otherwise cancelled the tap.
break;
case TAP_TYPE_TAP:
// The user has completed the tap gesture.
mConfig.setIsSmooth(!mConfig.isSmooth()); // 追加
break;
}
invalidate();
}


設定画面を作る

Watch上で設定できる設定画面を作ります。設定画面はActivityとして作ります。

新たにConfigActivityクラスを作ります。

public class ConfigActivity {

}


strings.xmlに設定画面のタイトルと設定項目を追加します。

<string name="title_activity_config">Config</string>
<string name="smooth">Smooth</string>


AndroidManifestにActivityを定義します。
IntentFilterを設定して設定から起動するように指定します。
action android:nameのorg.firespeed.myapplicationの部分はそれぞれのパッケージ名に置き換えてください。

<activity
android:name=".ConfigActivity"
android:label="@string/title_activity_config">
<intent-filter>
<action android:name= "org.firespeed.myapplication.CONFIG" />
<category android:name= "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>


Serviceにも設定画面が有ることをmeta-dataで定義します。
android:nameの値は上記のaction android:name=で指定した値に合わせてください。
 
<service
android:name=".MyWatchFace"
android:label="@string/my_analog_name"
android:permission="android.permission.BIND_WALLPAPER">
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face"/>
<!-- 四角い時計のプレビュー -->
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_analog"/>
<!-- 丸い時計のプレビュー -->
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_analog"/>

<!-- ここから追加 -->
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="org.firespeed.myapplication.CONFIG" />
<!-- ここまで追加 -->

<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService"/>
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE"/>
</intent-filter>
</service>




layoutフォルダに新たに設定画面のレイアウト用のactivity_config.xmlを作成します。
今回はCheckBoxを一つだけ作ります。
レイアウトの作成方法は普通のAndroidと同様です。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:id="@+id/smooth"
android:text="@string/smooth"/>
</FrameLayout>


ConfigActivityの作成

ConfigAcitivity.javaを開いて
Configを格納するためのメンバ変数を作成します。

private Config mConfig;


onCraete()をOverrideしてレイアウトを取得する定番の処理を追加してViewからチェックボックスを取得します。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_config);
final CheckBox smooth = (CheckBox)findViewById(R.id.smooth);
}


mConfigを初期化します。
フレームを再描画するタイミングで設定を見なおしていたWatchFaceと違って、ConfigActivityはフレームの書き換えがないため、Config.OnConfigChangedListenerで設定が取得・変更されたことを取得してチェックボックスの値を反映します。

mConfig = new Config(this, new Config.OnConfigChangedListener() {
@Override
public void onConfigChanged(Config config) {
smooth.setChecked(config.isSmooth());
}
});


逆にチェックボックスをチェックした時にConfigの値を書き換える処理を追加します。


smooth.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mConfig.setIsSmooth(isChecked);
}
});


Configの同期をonResume()で開始、
onPauseで終了します。

@Override
protected void onResume() {
super.onResume();
mConfig.connect();
}

@Override
protected void onPause() {
super.onPause();
mConfig.disconnect();
}


ConfigActivity全体は次のようになります。

package org.firespeed.myapplication;

import android.app.Activity;
import android.os.Bundle;
import android.support.wearable.view.WatchViewStub;
import android.util.Log;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;

public class ConfigActivity extends Activity {

private Config mConfig;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_config);
final CheckBox smooth = (CheckBox)findViewById(R.id.smooth);
mConfig = new Config(this, new Config.OnConfigChangedListener() {
@Override
public void onConfigChanged(Config config) {
smooth.setChecked(config.isSmooth());
}
});
smooth.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mConfig.setIsSmooth(isChecked);
}
});
}

@Override
protected void onResume() {
super.onResume();
mConfig.connect();
}

@Override
protected void onPause() {
super.onPause();
mConfig.disconnect();
}
}


WatchFaceをインストールして、Watch Faceを選択する画面に、歯車が追加されていることを確認してください。


歯車をタップすると設定画面が開きます。


右側にスワイプすると設定画面が閉じてWatch Faceに戻ります。
チェック画面に応じて秒針の動きが変わることを確認してください。
また、Watch Faceをタップして秒針の動きを変えた時も設定画面の値が反映されてデータが同期されることを確認できます。

このように設定を追加して画面タップや設定画面で動きをカスタマイズできるのもAndroid Wear Watch Faceの魅力です・
次回はスマートフォン側アプリを作成してスマートフォンでも設定が行えるようにします。
スマートフォン側で設定を変えることでより高度な画面操作も可能になります。
スマートフォン側アプリを開発することでGooglePlay Storeに公開することも出来るようになります。

Watch Faceを作る コンパニオンアプリの作成

$
0
0
Android WearのWatch Faceを作る 目次
今回はWearのWatchFaceと連携するスマートフォン側アプリを開発します。

スマートフォン側のアプリはモジュールmobileにて開発します。
Watch Faceのアプリは、Wear用のAPKとスマートフォン用のAPKが別々に動きます。

Wearアプリの修正

コンパニオンアプリを開くためのIntentを指定最初にWear側の設定を行います。
Wear側でスマートフォンから設定を受け付けるようにWearモジュールのAndroidManifestを修正します。
Serviceの配下に次の記載を追加します。この状態でWearにインストールするとWear側の準備は完了です。
org.firespeed.myapplication部分はパッケージ名で置き換えてください。

<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="org.firespeed.myapplication.CONFIG" />


スマートフォン側アプリを作る

スマートフォン側のアプリのことをコンパニオンアプリといいます。

コンパニオンアプリはWearとは異なるAPKとしてスマートフォンに直接インストールします。
Android Wearと接続されているスマートフォンを用意してUSBでPCと接続してください。

WearのWatchFaceとコンパニオンアプリはそれぞれ異なるモジュールのためお互いのソースコードをそのままでは参照できません。
しかし、Configで設定する内容はどちらも同じです。今回は一つだけですが、今後設定値を増やしたいと思った時にWatch Faceとコンパニオンアプリのどちらも設定を追加しないといけないのは大変です。
そのため、どちらからでも読み込めるようにConfigクラスを独立したbothモジュールとして切り離してみましょう。

bothモジュールの作成

モジュールの作成

Android Studioで
File -New - New moduleを選択します。


New ModuleダイアログでAndroid Libraryを選びます。


Application Library NameをBoth
Package nameをWearのPackage nameに合わせMinimumSDKを18とします。
FinishでModuleを作成します。


build.gradelの作成

bothモジュールが作成されたら
bothモジュールのbuild.gradleを選びdependencies から次を削除して

compile 'com.android.support:appcompat-v7:23.1.1'


以下を追加します。

compile 'com.google.android.gms:play-services-wearable:8.1.0'


bothモジュールを読み込めるようにwearモジュールとmobileモジュールの両build.gradleについてdependencies に

compile project(':both')

を追加します。

config.javaの移動

wearモジュールのsrc/main/java/パッケージネーム内にあるconfig.javaをbothモジュールのsrc/main/java/パッケージネーム内に移動します。


WearモジュールのConfigActivity.javaとMyWatchFace.java を開いてConfigクラスが見つけられずにエラーとなっているのでbothのパッケージ名にあるConfigをインポートします。

import org.firespeed.both.Config;


mobileモジュールの修正

画面の作成

Android Wearの設定画面作成は基本的に通常のActivityと同様の手順で行います。
まず、コンパニオンアプリを修正するためにmobileモジュールを開きます。
mobileモジュールを右クリックしてNew-Activity-Empty Activityを選択します。
Generate Layout Fileにチェックが入っていることと、Launcher Activityのチェックが外れていることを確認してFinishをクリックします。
res/values/strings.xmlを開いてチェックボックスのメッセージを追加します。

<resources>
<string name="app_name">My Application</string>
<string name="smooth">Smooth</string>
</resources>


res/layout/activity_main.xmlを開いてチェックボックスを追加します。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="org.firespeed.myapplication.MainActivity">

<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/smooth"
android:id="@+id/smooth"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"/>
</RelativeLayout>


MainActivity.javaを開いてチェックボックスの処理を追加します。
追加する内容はConfigActivityと同様です。

コンパニオンアプリを実行する。

メニューバーのモジュールでmobileを選択します。

mobileにエラーが表示されることが有ります。

これは通常のアプリと違ってLauncherから起動するActivityが存在せず、どのActivityを起動して良いのかがAndroid Studio側から不明なためです。
そこで、MainActivityを起動するように設定を行います。

メニューバーからRun-Edit Configurations...を選びます。


Generalタブになっていることを確認し、Launch OptionsをSpecified ActivityにしてActivityをMainActivityとします。


この状態で実行するとSmoothのチェックボックスが画面上に表示されます。
チェックボックスをタップして、リアルタイムにWatchFaceの針がなめらかに動いたり一秒ごとに動いたりすることを確認してください。
この通信は双方向のため、Watch Face側で設定を変更してもリアルタイムにコンパニオンアプリのチェックボックスが切り替わります。

デバッグではなく、通常の状態で起動するためにはAndroid Wearアプリをスマートフォンで開いてWatch Faceの選択画面で作成したWatch Faceを選ぶと中央にギアが表示されます。
ギアをタップするとMainActivityが開きます。


このようにコンパニオンアプリを用意することでスマートフォン側のリッチな環境を使ってより細かな設定を行うことが出来ます。
GooglePlay Storeに展示する場合は署名済みのAPKを作ってmobileモジュールのAPKのみを公開します。
署名済みmobile APKの中にはwear APKが内包されており、ユーザーがGooglePlay Storeからダウンロードすると自動的にmobileモジュールAPKがインストールされ、その中のwearモジュールAPKがwearに転送されインストールされます。

これで基本的なWatchFaceの作り方はおしまいです。

次回はいよいよ最終回。Watch Faceを作る上でのTipsを紹介します。

Watch Faceを作る その他Tips

$
0
0
Android WearのWatch Faceを作る 目次
連載Watch Faceを作ろう最終回はWatch Faceを作る上でよくあるTipsを集めました。

1.丸型と四角で処理を変えるには

Android Wearには丸型の画面を搭載したモデルと、四角い画面を搭載したモデルが有ります。
丸い画面か四角い画面かを判別したいときはEngineのonApplyWindowInsetsをOverrideして引数として渡されたWindowInsetsのisRound()を呼びます。
丸い画面の場合true、四角い画面の場合falseが返ります。

@Override
public void onApplyWindowInsets(WindowInsets insets) {
super.onApplyWindowInsets(insets);
boolean isRound = insets.isRound();
}


設定画面などでレイアウトを丸型と四角で切り替えたい時はstubを使用します。

<android.support.wearable.view.WatchViewStub
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rectLayout="@layout/rect_layout"
app:roundLayout="@layout/round_layout"/>


あとは、layoutフォルダにrect_layoutとround_layoutをそれぞれ作成します。
これによりstubは四角い画面ではrect_layoutに、丸い画面ではround_layoutに置き換わります。
丸い画面内にある四角い領域を切り取りたい場合は、BoxInsetLayoutも使用可能です。

<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent">
</android.support.wearable.view.BoxInsetLayout>

このViewの子要素は、四角い画面の場合は画面いっぱいが選択されれるのに対して、丸型の場合は、中央の正方形部分が切り取られます。
この場合、丸型の液晶は切り取られる部分が余白となるので、実質的にゆとりあるデザインになるのに対して、四角の場合は全画面が選択されるので窮屈な画面となる時があります。
BoxInsetLayoutにpaddingを設定することで、四角い画面でも丸い画面でも余白をつけることが出来ます。
この余白は、丸型の画面では切り取られる余白とどちらか大きなほうが適用されます。

<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:padding="15dp">
</android.support.wearable.view.BoxInsetLayout>



中央以外で針を回転させるには

画像で描かれた針の描画は針を水平移動してから中央地点を基準に回転させています。
同様に所定の位置に針を動かした後、基準にしたい位置を元に回転させます。
所定の位置への移動は「デザインした画像」=グラフィックソフト上での画像を元にmScaleを掛けた値にmBackgroundLeftあるいはmBackgroundTopを足します。
このため、針の画像は通常、mScaleでリサイズされたBitmap、水平移動するためのX座標、Y座標、回転中央市のX座標、Y座標を持つことになります。
これらは定形処理のため、クラスにまとめてしまうというのも手です。
例えば次のようなクラスにすることが出来ます。

package org.firespeed.myapplication;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;

/**
* Created by kenz on 2015/12/06.
*/
public class Hand {
private Bitmap mScaledBitmap;
private final int mBitmapId;
private final float mTop;
private final float mLeft;
private final float mCenterX;
private final float mCenterY;
private final float mScale;

private static Bitmap createScaledBitmap(Resources resources, int id, float scale) {
Bitmap original = ((BitmapDrawable) (resources.getDrawable(id))).getBitmap();
Bitmap scaled = Bitmap.createScaledBitmap(original, (int) (original.getWidth() * scale),
(int) (original.getHeight() * scale), true);
return scaled;
}

public Hand(Resources resources, int bitmapId, float scale, float backgroundTop, float backgroundLeft, float left, float top, float centerX, float centerY) {
mScaledBitmap = createScaledBitmap(resources, bitmapId, scale);
mLeft = left * scale + backgroundLeft;
mTop = top * scale + backgroundTop;
mCenterX = centerX * scale + backgroundLeft;
mCenterY = centerY * scale + backgroundTop;
mBitmapId = bitmapId;
mScale = scale;
}


public void rescaleBitmap(Resources resources) {
mScaledBitmap = createScaledBitmap(resources, mBitmapId, mScale);
}

public void draw(Canvas canvas, Paint paint, Matrix matrix, float rotate) {
matrix.setTranslate(mLeft, mTop);
matrix.postRotate(rotate, mCenterX, mCenterY);
canvas.drawBitmap(mScaledBitmap, matrix, paint);
}

public boolean isRecycled() {
return mScaledBitmap == null || mScaledBitmap.isRecycled();
}
}


前回作った時針、分針をこのクラスに置き換える場合はMyWatchFace.javaを次のように書き換えます。
mHourとmMinuteをBitmapからHourに置き換えます。

private Bitmap mHour; // 削除
private Bitmap mMinute; // 削除
private Hour mHour; // 追加
private Hour mMinute; // 追加

個別に保存していた座標を削除します。

private float mHourLeft;
private float mHourTop;
private float mMinuteLeft;
private float mMinuteTop;

onDraw()にて縦横の座標を求めていた部分をHandのインスタンス生成に置き換えます。
Handはコンストラクター内で座標計算を行います。
// 以下削除

mHourLeft = 244f * mScale + mBackgroundLeft; // 削除
mHourTop = 80f * mScale + mBackgroundTop; // 削除
mMinuteLeft = 242f * mScale + mBackgroundLeft; // 削除
mMinuteTop = 54f * mScale + mBackgroundTop; // 削除

// 以下追加
Resources resources = getResources();
mHour = new Hand(resources, R.drawable.hour, mScale, mBackgroundTop, mBackgroundLeft, 244f, 80f, 256f, 256f);
mMinute = new Hand(resources, R.drawable.minute, mScale, mBackgroundTop, mBackgroundLeft, 242f, 54f, 256f, 256f);


画像が破棄された時に備えたロジックもメソッドを呼ぶだけになります。

if (isSizeChanged
|| mBackground == null || mBackground.isRecycled()
|| mHour == null || mHour.isRecycled()
|| mMinute == null || mMinute.isRecycled()) {
Resources resources = getResources();
mHour = createSaledBitmap(resources, R.drawable.hour); // 削除
mMinute = createSaledBitmap(resources, R.drawable.minute); // 削除
mHour.rescaleBitmap(resources); // 追加
mMinute.rescaleBitmap(resources); // 追加
mBackground = createScaledBitmap(resources, R.drawable.background);
}


描画について、Matrixの処理もHandクラス内で行っているためCanvasとPaintとMatrix、そして角度を渡すだけです。

float hourRotate = (mCalendar.get(Calendar.HOUR) + mCalendar.get(Calendar.MINUTE) / 60f) * 30;
mMatrix.setTranslate(mHourLeft, mHourTop); // 削除
mMatrix.postRotate(hourRotate, mCenterX, mCenterY); // 削除
canvas.drawBitmap(mHour, mMatrix, mBitmapPaint); // 削除
mHour.draw(canvas,mBitmapPaint, mMatrix, hourRotate); // 追加
float minuteRotate = (mCalendar.get(Calendar.MINUTE) + mCalendar.get(Calendar.SECOND) / 60f) * 6;
mMatrix.setTranslate(mMinuteLeft, mMinuteTop); // 削除
mMatrix.postRotate(minuteRotate, mCenterX, mCenterY); // 削除
canvas.drawBitmap(mMinute, mMatrix, mBitmapPaint); // 削除
mMinute.draw(canvas,mBitmapPaint, mMatrix, minuteRotate); // 追加

このようにクラスにまとめておくことで、針の数が増えても闇雲にMyWatchFaceを複雑にせずに実装することが出来ます。

Ambient Modeで画像を使う方法

Ambient Modeで画像を使う場合、本来であれば白黒で作るのが望ましいです。
白黒の画像を作ったらHandクラスを書き換えてAmbientモード時は白黒のBitmapとなるように修正します。

package org.firespeed.myapplication;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;

/**
* Created by kenz on 2015/12/06.
*/
public class Hand {
private Bitmap mScaledBitmap;
private Bitmap mAmbientBitmap;
private final int mBitmapId;
private final int mAmbientBitmapId;
private final float mTop;
private final float mLeft;
private final float mCenterX;
private final float mCenterY;
private final float mScale;

private static Bitmap createScaledBitmap(Resources resources, int id, float scale) {
Bitmap original = ((BitmapDrawable) (resources.getDrawable(id))).getBitmap();
Bitmap scaled = Bitmap.createScaledBitmap(original, (int) (original.getWidth() * scale),
(int) (original.getHeight() * scale), true);
return scaled;
}

public Hand(Resources resources, int bitmapId, int ambientBitmapId, float scale, float backgroundTop, float backgroundLeft, float left, float top, float centerX, float centerY) {
mScaledBitmap = createScaledBitmap(resources, bitmapId, scale);
if(bitmapId == ambientBitmapId) {
mAmbientBitmap = mScaledBitmap;
}else{
mAmbientBitmap = createScaledBitmap(resources, ambientBitmapId, scale);
}
mLeft = left * scale + backgroundLeft;
mTop = top * scale + backgroundTop;
mCenterX = centerX * scale + backgroundLeft;
mCenterY = centerY * scale + backgroundTop;
mBitmapId = bitmapId;
mAmbientBitmapId = ambientBitmapId;
mScale = scale;
}

public void rescaleBitmap(Resources resources) {
mScaledBitmap = createScaledBitmap(resources, mBitmapId, mScale);
if(mBitmapId == mAmbientBitmapId){
mAmbientBitmap = mScaledBitmap;
}else{
mAmbientBitmap = createScaledBitmap(resources, mAmbientBitmapId, mScale);
}
}

public void draw(Canvas canvas, Paint paint, Matrix matrix, float rotate, boolean isAmbient) {
paint.setFilterBitmap(!isAmbient);
matrix.setTranslate(mLeft, mTop);
matrix.postRotate(rotate, mCenterX, mCenterY);
canvas.drawBitmap(isAmbient?mAmbientBitmap:mScaledBitmap, matrix, paint);
}

public boolean isRecycled() {
return mScaledBitmap == null || mScaledBitmap.isRecycled() || mAmbientBitmap == null || mAmbientBitmap.isRecycled();
}
}

Handのインスタンス生成時に白黒のリソースIDを渡すように処理を追加します。

mHour = new Hand(resources, R.drawable.hour, R.drawable.hourWb, mScale, mBackgroundTop, mBackgroundLeft, 244f, 80f, 256f, 256f);
mMinute = new Hand(resources, R.drawable.minute,R.drawable.minuteWb, mScale, mBackgroundTop, mBackgroundLeft, 242f, 54f, 256f, 256f);

draw()にAmbientかどうかを渡すようにします。

float hourRotate = (mCalendar.get(Calendar.HOUR) + mCalendar.get(Calendar.MINUTE) / 60f) * 30;
mHour.draw(canvas,mBitmapPaint, mMatrix, hourRotate, mAmbient);
float minuteRotate = (mCalendar.get(Calendar.MINUTE) + mCalendar.get(Calendar.SECOND) / 60f) * 6;
mMinute.draw(canvas,mBitmapPaint, mMatrix, minuteRotate, mAmbient);


AmbientModeに入るまでの時間を調整する

※この設定はAndroidWearのバッテリー持ちに大きな影響をおよぼす場合があります。
使用する場合はデフォルトをオフとして、設定で有効にできるようにすることをおすすめします。
また、この方法は非推奨の機能を使用しています。将来的なバージョンアップで使用できなくなる可能性があります。

アニメーションをみせるWatchFaceではAmbientModeへ入る時間が短すぎることが有ります。
AmbientModeに入る時間はPowerManager.WakeLockのacquire()を使って指定することが出来ます。
acquire()の引数としてAmbientModeへ入るまでの時間をミリ秒で渡します。
もし、引数を指定しなかった場合無制限となります。 必ずreleaseを読んで明示的にWakeLockを解除してください。


private static final String WAKE_LOCK_TAG = "my_watch_tag";
private static final long WAKE_LOCK_TIME = 20000l; // ambient modeに入る時間(ミリ秒)
private void setWakeLock() { ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, WAKE_LOCK_TAG).acquire(WAKE_LOCK_TIME);
}


setWakeLock()をAmbientModeから出た時と画面を表示した時にそれぞれ呼び出します。
AmgientModeから出た時

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if(mAmbient) {
setWakeLock(); // 追加
mConfig.connect();
}else{
mConfig.disconnect();
}
if (mLowBitAmbient) {
mDrawPaint.setAntiAlias(!inAmbientMode);
mBitmapPaint.setFilterBitmap(!inAmbientMode);
}
invalidate();
}
// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}

画面が表示された時

@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);

if (visible) {
registerReceiver();
// Update time zone in case it changed while we weren't visible.
mCalendar.setTimeZone(TimeZone.getDefault());
mConfig.connect();
setWakeLock(); // 追加
} else {
unregisterReceiver();
mConfig.disconnect();
}

// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}



テキストの表示方法

今回はアナログ時計を作成しましたが、デジタル時計を作ったりニュースを表示するためにテキストを表示したい場合もあると思います。今回はmDrawPaintを使ってテキストを書いてみます。

onDraw()にてisSizeChangedのタイミングで文字のサイズを指定します。


@Override
public void onDraw(Canvas canvas, Rect bounds) {
mCalendar.setTimeInMillis(System.currentTimeMillis());
boolean isSizeChanged = canvas.getWidth() != mWidth || canvas.getHeight() != mHeight;
if (isSizeChanged) {
// 中略
mScale = longSize / DESIGNED_SIZE;
mDrawPaint.setTextSize(48f * mScale); // 追加
// 中略


onDraw()の最後にテキストを描画する処理を追加します。
引数は表示するテキスト、X座標(左端)、Y座標(下端)、paintです。

canvas.drawText("HELLO_TEXT", 0, 400 * mScale, mDrawPaint);


テキストをセンタリングする

テキストを左右中央寄せで表示したいという時もよくあると思います。
drawTextは左端を指定する必要があるのでテキストのサイズを調べて中央位置からその半分のサイズを引く必要があります。

PaintのmeasureText()を指定することでフォントと文字に応じた幅をしらべることが出来ます。
PaintにtextSizeが指定されている状態で、引数に計測したい幅の文字列を渡します。

String text = "HELLO_TEXT";
float x = mCenterX - (mDrawPaint.measureText(text) / 2f);
canvas.drawText(text, x, 400 * mScale, mDrawPaint);


その他のTips

その他お問い合わせはFacebookなどでお受けしております。
汎用的なTipsはこちらに追加していきたいと思います。

baserCMSのコンテストでノージャンル賞を受賞しました

$
0
0
今回は開発する上で最初に考えたのは、今Webに必要とされているのは何かということ。
その上で必要な機能を洗い出しました。

想定するターゲット層

初期セットアップとカスタマイズは専門知識を持った人が行い、一度セットアップが行われた後はユーザーが単独で保守していくという運用を想定しています。

見た目を変えやすくする

一番最初に考えたことがカスタマイズ性。
多人数が使うテーマの場合、そのまま使うとどこかで見たサイトそのままになってしまうため、実際には見た目についてカスタマイズを行うことが多いと思います。
そこで見た目については未完成な状態として、カスタマイズで完成してもらうことを想定とした設計としました。
具体的にはHTMLとCSSの完全な分離。サンプルとしてのCSSの提供。Media QueryとJava Scriptの禁止です。

見た目が未完成である以上、見た目のためのコードは最小限の範囲、つまりCSS内に抑えなくてはいけません。
HTMLに見た目の要素が入らないよう、最初にHTMLを作成して、その後CSSを作成しました。
CSS作成時にはHTMLには一切手を付けないこととして、装飾の要素をHTMLに入れられなくしました。

最後にHTMLが様々なデザインに適用可能であることを確認するためにCSSだけ変えて角ばった1カラム式と丸を主体としたマルチカラム式の2つの全く異なる見た目に仕上げることできることを確認しました。

そのためHTMLにはdivタグやclass="red"のような装飾のためのコードが一切含まれていません。(管理画面部を除く)
初期データをdefaultとwhiteで切り替えることで全く違う見た目になることが確認できます。


そのCSSですがサンプルとして提供するために見やすさを最重要視して、どちらのcssも200行以内に収まるようにしています。
実際のコードはgithubで確認できます。
default CSS
white CSS

Media QueryとJava Scriptはコードの見通しが悪くなるため管理画面を除いて全面的に禁止しました。
これらはいつ発動するかが見えづらく、カスタマイズする人が想定していない動きを起こす可能性があるため、カスタマイズを行った後で独自に追加してもらうのが良いと思います。

このような結果幾つかの見た目について妥協しています。
実装のために見た目を犠牲にするのは、実際にお客さんがいる案件では難しく、好きにやることが出来て楽しかったです。

一方で見た目以外の部分はそれほどカスタマイズされないだろうという前提で作りこみをこなっています。

多環境対応

環境に依存しないHTMLを書くことでMedia QueryやJavaScriptに頼らずレスポンシブデザインを組んでみました。
その結果AndroidでもiPhoneでもかなり古い端末までサポートすることが出来ました。

スマートフォンも廉価なモデルを含めて高解像度なディスプレイを搭載するのが当たり前になっていて、そういう端末で美しく見えるようRetinaに対応しました。
特殊な方法として、ブログのアイキャッチに写真をアップロードするときにRetina用の画像が自動生成されるようになっています。
これは本体プラグインの機能なのですが、スマートフォン向けに画像を自動縮小出来る機能を利用して、PC向けの解像度とスマートフォン向けの解像度を2:1に設定することで、PC用の画像をretina、スマートフォン用の画像を非retinaとなるようにしたうえで、画面密度に応じて開く画像が切り替わるsrcsetを出力するようにしました。


ソーシャル対応とSEO対応

ソーシャルとSEOに備えてOGPやmicrodataの組み込みを行ったほか、全てのページがランディングページとなりうるという前提でコンテンツファーストの作りとしています。

トップページも目次と割り切り、特殊なコンテンツを配置せず、最新ブログの記事を表示するようにしています。

保守性の確保

運用が始まった後の保守は、ソースコードを触れない人が管理画面で行っていくことを想定しています。
そのために、管理画面を大きく修正して初期セットアップ時に使う機能を目立たなく、ブログ投稿などの日常良く使う機能を目立つように配置しました。

標準の管理画面

カスタムされた管理画面
固定ページやウィジェット管理のようなあまり使わない項目を目立たなくして、代わりにブロクの記事を追加する「ページ追加」や記事を修正する「ページ管理」を追加。


また、管理画面をスマートフォンに対応させました。

ブログ主体のサイト

今回もコンテンツの修正はブログのみで行う前提になっています。
baserCMSでは固定ページとブログの管理がバラバラになっているため、混在して管理が煩雑になるより、気軽に更新できるブログ一本にまとめました。
ブログを使いやすくすることで、固定ページも使おうと思えば使えますが、暗黙的に使用しないような作りとしています。

管理者用バーのレイアウト調整

baserCMSは管理者でログイン済みの場合、上部に帯が付くのですが、これにより管理者は訪問者と見た目に差があるサイトを見ることになってしまいます。
そこで、管理者用のバーは通常非表示としてマウスオーバーした時だけ表示する仕様としました。
通常時は小さな▲が表示されるだけで管理バーが表示されない。


画面上部にカーソルを合わせると管理バーが表示される。


受賞して

様々な方々のアドバイスのお陰でこの賞を受賞することが出来ました。お礼申し上げます。
特に昨年のコンテストで共作してデザインのいろはをおしえてくれたnada designの坂田さん、Typoを見つけていただいた平田さんにとても感謝しています。
かなり変わった趣向のテーマだとは思うのですが、評価していただいた審査員の方々有難うございます。

今回はカスタマイズ性を最重要視してどんなサイトにも化けるサイトにしたかったのでノージャンル賞というのはまさに狙ったところに行けたのかなと思っています。(去年もノージャンル賞でしたが)

ソースコードはgithubにて公開しております。
Viewing all 342 articles
Browse latest View live