fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

2016年に見たアニメ

去年も書いたので今年の分も書いて振り返る。読みが五十音順になるよう並べていますが、そうなっていないところがもしあればそれは間違いです。

冬クール

アクティヴレイドとか昭和元禄落語心中は放送が終わった頃にようやく録画したやつを一気に見たけど素晴らしかった。他にもグリムガルとかGATE、デュラララ、僕だけがいない街、だがしかし、亜人暗殺教室、しょこめざ、ディメンションW、ルパン、などなど好きな感じのやつが多かったクールだったと思う。

春クール

カバネリ、ヒロアカ、クロムクロジョジョうしおととらなど熱い展開の話が多かったかな。クロムクロは自分のよく知っている風景がアニメに出てくるのをみるとなんとなく嬉しい感じになってよかった。忍殺はニコニコで1話を見たとき全然理解できなくてそれ以降見てなかったけど、今回改めて見たら最高にいかれててよかった。

夏クール

アクティブレイドが王道を突き進んで堂々と終わったのが最高に良かった。アルデラミンは二期を早く見たいなあ。ぞい!とかReLIFEは今の自分に鋭利に刺さる感じの話しで個人的にはつらかったけどおもしろかった。斉木楠雄のΨ難は純度の高い良質な神谷浩史成分を摂取できるアニメだった。

秋クール

ユーフォが3話ごとぐらいに神回をもってきてすごかったし黒沢ともよの演技もすごかった。イゼッタ、ドリフ、オカン、SBR#、舟を編む、ブレイブウィッチーズあたりもすごく好きで、いっせいに終わってしまうのが寂しくもある。わたモテは完全に女性向けだと思うんだけど、振り切れた小林ゆうとそれに付き合って振り切れる沢城みゆきを楽しめたのでよかった。

再放送枠

ボトムズに関してはtvk GJ!というより他ない。

Web

最後に宮沢賢治の「星めぐりの歌」が流れて無事に涙腺崩壊した。

劇場版枠

もっと見た気がしてたけど去年より2本多いぐらいだった。アニメ以外ではシン・ゴジラと聖の青春を見ているので例年よりは映画を見ていると思われる。どれも最高に面白かった。MJPは二期も見てみたい。

参考:2016年度の人気アニメランキング【あにこれβ】

ImageSpanを使って画像をTextViewの中にいい感じに表示する

(これはAPI Level 25(Android 7.1.1)のNexus5XおよびAPI Level 23(Android6.0)のNexus5(エミュレータ)で確認した)

TextViewsetCompoundDrawables(Drawable, Drawable, Drawable, Drawable)を使うと文字領域の上下左右に一つずつDrawableを置くことができるが、文章の中に画像を置いたり、例えば右の領域にDrawableを二つ並べて置くということはできない。そんな時の代替手段の一つにImageSpanがある。HTMLのimgタグに相当するものだろうか。

https://developer.android.com/reference/android/text/style/ImageSpan.htmldeveloper.android.com

これを使って、

RT by [アイコン画像] @screen_name

とか、

ユーザ名 @screen_name [認証マーク] [鍵マーク]

のように文字と画像とを一列に並べようとしていたところ、画像の底辺をどこに合わせるか指定する引数にALIGN_BOTTOMを選ぶ(デフォルト)と上の方に不思議な隙間ができてしまったのでどんな処理をしているのか見てみることにした。軽くぐぐってみると高さが合わない問題はすでに認識されている様子(たとえば、複数行のTextViewでImageSpanが下付きに描画される事象を回避する - Qiita)。この情報にならってDynamicDrawableSpan#draw()をオーバライドして解決を試みようとしたのだが、問題はどうもDynamicDrawableSpan#getSize()の方にあるのではないかという気がした。その理由を次から説明する。

DynamicDrawableSpan#draw()の引数の意味

DynamicDrawableSpan | Android Developersをみると引数についての説明がある。ここで重要なのは次の三つ。

  • top: 行(Canvas)の上辺(ふつう0?)
  • y: ベースライン
  • bottom: 行(Canvas)の底辺

DynamicDrawableSpan#getSize()では何をやっているのか

Cross Reference: /frameworks/base/core/java/android/text/style/DynamicDrawableSpan.java

(2016/12/19 12:28 追記)親クラスgetSize()draw()の説明がある:Cross Reference: /frameworks/base/core/java/android/text/style/ReplacementSpan.java

getSize()の戻り値はintなのだが、これはdraw()で描こうとするものの幅がどれだけかという意味。なのでgetDrawable().getBounds().rightを返せばよいだけのはずなのだが、ついでに引数であるPaint.FontMetricsIntの中身を書き換えている。具体的には次に挙げる変数の値を書き換えているのだが、これはdraw()で描こうとするものの高さ方向のデータになる。

  • top: グリフの中で最も背の高い文字の、ベースラインから上側の距離(負の数)
  • ascent: 一つの文字で推奨されるベースラインから上側の距離(負の数)
  • descent: 一つの文字で推奨されるベースラインから下側の距離(正の数)
  • bottom: グリフの中で最も足が長い文字の、ベースラインより下側の距離(正の数)

ちなみにこの説明は、Paint.FontMetricsの方にしか書いてない。topascentが負の数である理由は、スクリーンの座標系が上から下に向かって大きくなっているためらしい。ここで設定した値に応じてdraw()topybottomの値が決まるようである。

getSize()の元の処理は、ベースラインから上側の距離をDrawableの高さと等しくして、ベースラインから下の距離を0に設定している。その上でdraw()ではbottomDrawableの底辺を揃えて、ALIGN_BASELINEの時にはさらにベースラインからdescentの分だけ上に引きあげようという計算をしている。つまり、ベースラインの上側にDrawableの高さ分のスペースを確保しているのに、それよりさらに低いbottomの位置に底辺を合わせているので、ALIGN_BOTTOMの時にはbottomの分だけDrawableの上に隙間ができるということである。その行の他の文字がベースラインの下側を使っているために、getSize()で指定したbottom=0が却下されてしまうのではないかと思われる。

上の隙間をなくすには

画像の領域をベースラインの上側にのみ確保しようとしているのがそもそもおかしいのではないか。そうしたいのはALIGN_BASELINEを指定した時だけのはず。なので単純に次のようにしてみた。ポイントはDrawableのベースラインをどこにするか。getCachedDrawable()privateメソッドなので再定義する必要がある。

(2016/12/21 22:37:paint.getFontMetrics()を使って計算するよう修正)

  @Override
  public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
    final Rect bounds = getCachedDrawable().getBounds();
    if (fm != null) {
      final Paint.FontMetrics fontMetrics = paint.getFontMetrics();
      final int verticalAlignment = getVerticalAlignment();
      if (verticalAlignment == ALIGN_BASELINE) {
        fm.ascent = -bounds.bottom;
      } else if (verticalAlignment == ALIGN_BOTTOM) {
        fm.ascent = (int) (bounds.bottom * fontMetrics.top / (-fontMetrics.top + fontMetrics.bottom));
      }
      fm.descent = Math.max(bounds.bottom + fm.ascent, 0);
      fm.top = fm.ascent;
      fm.bottom = fm.descent;
    }
    return bounds.right;
  }

  @Override
  public void draw(Canvas canvas,
                   CharSequence text, int start, int end,
                   float x, int top, int baseline, int bottom,
                   Paint paint) {
    final Drawable cachedDrawable = getCachedDrawable();
    final int drawableHeight = cachedDrawable.getBounds().bottom;

    canvas.save();
    int transY = top;
    if (getVerticalAlignment() == ALIGN_BASELINE) {
      transY = baseline - drawableHeight;
    } else if (getVerticalAlignment() == ALIGN_BOTTOM) {
      transY = bottom - drawableHeight;
    }
    canvas.translate(x, transY);
    cachedDrawable.draw(canvas);
    canvas.restore();
  }

例に挙げた先のページでは、複数行のTextViewでline spacingを1.5に設定すると画像が下付き文字のようになってしまうという現象が確認されているが、これはdraw()の引数であるbottomが行間の高さも含んでいるためではないかと考えられる。Paint.FontMetricsIntは行間の高さをleadingという変数名で持っている。このleadingの分だけ上に移動させてやることで意図した通りに表示できるのではないかと思われる。上のソースでいうと

      transY = bottom - drawableHeight;

      transY = bottom - drawableHeight - paint.getFontMetricsInt().leading;

にするという感じ。これは手元では確認していないので間違っているかもしれない。

今後の展望

中央揃えができたらいいなと思っている。一連のソースは[WIP] introduced RetweetUserView to replace RT user container by akihito104 · Pull Request #166 · akihito104/UdonRoad · GitHubに上がっており、そのうちマージされる予定。パッチ書いてAOSPに取り込まれるのが一番いいんだと思うんだけどいろんなケースに対応できるかどうかはちょっと自信がない。でもチャレンジしてみるのもいいのかな。

関係ありそうなissueとか

Issue 21397 - android - ImageSpan ALIGN_BASELINE works incorrectly on last line only: caused by bug in DynamicDrawableSpan - Android Open Source Project - Issue Tracker - Google Project Hosting

android - Align text around ImageSpan center vertical - Stack Overflow

Html.fromHtml()はカジュアルに使うものではない?

拙作のツイッタークライアントが、起動して3時間ほど放置しておくと全く動かなくなってしまうようになった。どこか触るとANRが出て落とせるので、その都度traces.txtを見ると、いつもHtml.fromHtml()を呼んでるところで止まっている様子。

内部ではパーサーオブジェクトのイニシャライズでnew char[2000]をやっているところだった。

Cross Reference: /external/tagsoup/src/org/ccil/cowan/tagsoup/Parser.java

名前やコメントから察するにこの配列はHTMLのコメント部分を読み込む時に使うバッファのようなのだが、メンバ名で検索しても宣言の箇所しかヒットしない。可視性がprivateなのでなんのためにあるのかわからなかった。

traceviewで見ると処理時間も結構かかるようだし、せいぜい1行のTextViewに使うには大げさなのかなと思ってなるべく呼ばないようにする工夫を入れた。固まってしまう原因はまだよくわからないので経過を見守ることにする。

(これはAndroid 6.0で確認した)