Twitterクライアント`aoeliyakei`のベータ版をリリースした
自分で使うために1年半ほどずっと作っていたTwitterクライアントのβ版をplayアプリストアでオープンベータ版として公開した。FABをフリックしてRTとかlikeとかを行うクレイジーな操作感のアプリである。リポジトリ名はUdonRoad
だがアプリとしてリリースするにあたり真剣に考えた方がよかろうということでaoeliyakei
という名前をつけた。
aoeliyakei
(アオエリヤケイ)という名前に決めるまでには実は紆余曲折あったのだが、かいつまんで話すと次のような流れである。
- 鳥の名前にしたいとは思っており、なんとなくフクロウをイメージしていたがすでにいろんなアプリのモチーフに使われているのでやめた
- あるときたまたまホロホロ鳥が目に入り、FABを操作するアプリなのでホロホロ鳥の丸っこいシルエットとイメージが合って良さそうだなと思ってそれに近い感じの名前を色々と模索したが(holoholoとかholoroとか、漢字では珠鶏と書くようなのでtamadoriとか)よく似た感じの人名や焼き鳥屋さんなどがヒットしたのでやめた
- ホロホロ鳥は英語でguinea fowlというのだが、fowlは家禽(鳥の家畜)という意味らしく、自家用のTwitterクライアントとして作っていたこのアプリにぴったりでは?と思ってfowlを生かすことにした
- もともとは毎日コードを書いてgithubに上げようというのが目的だったのでcontributionの緑色のタイルの要素を入れようと思ってgreenを生かすことにした
- “green fowl"でぐぐったらgreen junglefowl(=アオエリヤケイ)とかいうクッソ格好いい鳥が出てきたので採用
アプリアイコンは頭文字のaを鳥っぽいデザインにして背景に緑を使った。マテリアルアイコンっぽくなるようにedge tintや陰影をつけてみた。青色を入れたのは本家リスペクト。
自分のために作っていたので、自分で使わないような機能は作り込みが甘いか、またはまだ実装していない*1。それでもとにかく出すことにしたのは、それをやって初めてスタートラインに立ったことになるのではないかと思ったから。完成させて人に見てもらう、使ってもらうから上手になっていけるのかな、とMFTやコミケを回っているうちにそんな風なことを考えたりした。出来上がっているものがとても眩しく見えた。Twitterクライアントアプリとしての機能は少ないかもしれないが、自分はそんなに不自由せずに使えているので、ここまでの成果をいろんな人に見てもらえるようにした。
FABをフリックするというのが受け入れられるかどうか不安で、最初は出すべきではないとも思っていたが、自分の感覚だけで結論を出すのもよくないような気もしたのでいろんな人に試してもらって意見が欲しい。
https://play.google.com/apps/testing/com.freshdigitable.udonroad
*1:リスト関連とかDMとか
ViewPagerの中に入れたImageViewで拡大縮小やスクロールをするためにやったこと
UdonRoadではツイートに添付されている画像を表示させるため、ViewPager
の中にImageView
を入れて使っている。これまでは特に何も考えず表示させていたが、パノラマっぽい横長の画像やマンガの中の小さな文字などを詳しく見るために画像を拡大できるようにしたくなったので次のような処理を追加した。
ViewPager
は左右のフリックジェスチャを受け取ってページを切り替えるため、それと競合するような横方向のジェスチャを受け取って処理したいとなると工夫が必要である。具体的には次の二つ。
- 真横方向のピンチイン/アウト
- 真横方向のスクロール(ドラッグ)
タッチイベントを受け取った後の処理の流れを大雑把に説明すると次のような感じ。画像の拡大縮小、移動でMatrix
を使っている理由は特にありません。
onTouchEvent
ScaleGestureDetector.onTouchEvent()
にMotionEvent
を渡してピンチイン/アウトのジェスチャを認識させるGestureDetector.onTouchEvent()
にMotionEvent
を渡してスクロール(ドラッグやスワイプ)ジェスチャを認識させる- ピンチイン/アウトまたはスクロールイベントが起きたらスケールファクタや移動量を受け取り、"画像変換のための
Matrix
“を更新する ViewParent.requestDisallowInterceptTouchEvent(true)
を呼ぶView.invalidate()
を呼んでviewを更新する
onDraw
- 画像の現在の
Matrix
をImageView.getImageMatrix()
で取得して"画像変換のためのMatrix
“をMatrix.postConcat()
で渡す - スケールの値が初期値より小さくならないよう補正する
- 画像が縦横方向に移動しすぎてフレームアウトしないように(若しくは、はみ出すようなサイズのときに背景が見えないように)補正する
- 補正した
Matrix
をImageView.setImageMatrix()
で渡す - “画像変換のための
Matrix
"をリセットする
- 画像の現在の
肝はViewParent.requestDisallowInterceptTouchEvent(true)
で、ImageView
で消費したいイベントが起きた時にこれを呼ばないと親のView
(ここではViewPager
)にイベントを取られてしまう。
あとは細かい処理やハマりポイントとして、次の2点を挙げておく。
- 隣のページに行きたそうなドラッグは
ImageView
で受け付けずに親に渡す(shouldGoNextPage()
みたいな名前のメソッドで実装している) ScaleGestureDetector.OnScareGestureListener.onScale()
にfalse
を返してもScaleGestureDetector.onTouchEvent()
はtrue
を返してくる。スクロールが起きているかどうかを知りたかったらScaleGestureDetector.isInProgress()
を呼ぶ
この記事の全ての事象はNexus5X(Android 7.1.2)で確認した。
ExoPlayerのAudioProcessorを実装する
頭外音像定位のデモためのAndroidアプリを作っている。
音に独自の効果を加えたいときはAudioProcessor
を実装する。AudioProcessor
は前段の処理で展開された音データをPCM形式で受け取り、これを変換して返す。返したデータはその後、ビデオとの同期を取ったり再生可能な形式に変換されたりして、AudioTrack
に書き込まれる。今回のアプリで実装した主なメソッドは次の通り。
boolean configure(int, int, int) throws UnhandledFormatException
渡されるPCMデータのサンプリング周波数、チャンネル数、エンコード形式の種類を受け取る。処理可能なら渡された値を覚えておいて、設定が変わったらtrue
、変わらなかったらfalse
を返す。もし処理できない形式だったら、UnhandledFormatException
を投げる。このアプリでは44100Hz, 2ch, 16bitの形式だけ受け付けることにしている。
int getOutputEncoding()
出力するエンコード形式の種類を返す。使用可能なエンコード形式は@C.Encoding
な定数として定義されている。このアプリではC.ENCODING_PCM_16BIT
を返している。
int getOutputChannelCount()
出力するデータのチャンネル数を整数で返す。このアプリでは2を返している。
void queueInput(ByteBuffer)
音データのPCMをByteBuffer
で受け取って処理する。処理したデータはここではなく後述するgetOutputBuffer()
で返す。最終的な計算結果は適当なByteBuffer
なフィールドに入れておく。このアプリはエフェクトをかけるかどうかのスイッチを持っていて、これを切り替えることでエフェクト有り/無しの音を聞き比べられるようにしている。
エフェクト有りの場合
エフェクト有りの場合はLチャンネルとRチャンネルのそれぞれにHRTFをかける。左側の図のようなステレオ音場を再現するためには、右側の図の計算イメージで示すように4回の畳み込み計算を行わなければならないのだが、この処理が結構重いため、1つの畳み込みを1つのスレッドに割り当てて、4つのスレッドを使って並列に処理している。
図(左)再現しようとしている音場、(右)計算のイメージ
また、時間領域における畳み込み計算は周波数領域における乗算で置き換えられることから、FFTとIFFTを使っている。FFTってそういえばちゃんと書いたことなかったな…と思い立って、今回は外部のライブラリを使わず自分で実装した。畳み込み計算をすると音データが長くなるので、元の長さの分だけ返すようにする。はみ出た分は次のフレームの計算結果に足しあわせる。
エフェクト無しの場合
エフェクト無しの場合は受け取ったデータをほぼそのまま返す。単にそのまま返してしまうとエフェクト有りの時との音量差が大きいので、ちょっと小さくしている。また、前のフレームがエフェクト有りだった時には畳み込み計算の結果がオーバーラップしてくるのでその分を足しあわせてやる必要がある。
ByteBuffer getOutput()
queueInput()
で作ったByteBuffer
オブジェクトを渡す。一度渡したオブジェクトが2度と渡されないよう、AudioProcessor.EMPTY_BUFFER
を使って置き換えておく必要がある。