読者です 読者をやめる 読者になる 読者になる

おでんはじめました。

required ちくわぶ and 巾着,optional はんぺん.

Xamarin.Formsで縦組み

Xamarin Advent Calendar 2016(その2)の21日目です。

はじめに

10月にJXUGC #17 お前の Xamarin アプリを見せてみろ!に登壇させていただいたのでそのまとめです。

goo.gl

なぜXamarin.Formsで縦組みか

最近のHTML+CSS縦組み機能が充実しており大抵のブラウザで表示可能です。ただしブラウザによってその実装はまちまちで、端末に依存することなく同じイメージで表示したいというのが始めたきっかけです。自分で実装することで例えば左にルビを表示する(そんな人はほとんどいないでしょうけど)など自由に機能追加できることが(当たり前ですが)魅力の一つです。

SkiaSharp

縦組みの描画は2Dの描画フレームワークSkiaSharpを使用しています。Xamarinが推奨してるわりに日本語情報は少ないのですが、最近になって記事も増えてきたので今後に期待しています。今回のサンプルではRendererを使ってAndroidiOSをそれぞれ実装していますが、最新バージョンではXamarin.Formsに対応しています。ということで早く書き直さないと(汗)。

日本語フォント

日本語フォントを表示するためにIPAIPAex明朝フォントを使用しています。サイトからダウンロードしたipaexm.ttfをリソースフォルダ(AndroidはAssetsフォルダ、iOSはResourcesフォルダ)へ追加しています。

縦組みエンジン(Hanako)

タグの読み込みと組版(Compose)はHanakoという縦組みエンジンで実装しています。この中で行末の折り返しやページごとに組版をおこなうといった処理をおこなっています。

縦組み描画コントロール(TategumiView)

組版されたページをTategumiViewコントロールで表示しています。SkiaSharpで用意されたCanvasにDrawTextで1文字ずつXYを指定して描画しています。

//文字の描画
static void drawHonbunChar(SKCanvas canvas, SKPaint paint, WaterTrans.TypeLoader.TypefaceInfo tfi, HKWaxBase ch)
{
  //文字をGlyphIdに変換
  var glyphs = stringToVerticalGlyphs(ch.Char, paint, tfi);
  //描画の文字サイズを設定
  paint.TextSize = ch.FontSize;
  //本文を描画
  drawText(canvas, glyphs, ch.DevX, ch.DevY, paint);
}
static unsafe void drawText(SKCanvas canvas, ushort[] glyphs, float x, float y, SKPaint paint)
{
  paint.TextEncoding = SKTextEncoding.GlyphId;
  fixed (ushort* p = glyphs)
  {
    canvas.DrawText((IntPtr)p, glyphs.Length * 2, x, y, paint);
  }
}

縦組み用の字形

上記の処理でstringやcharではなくGlyphIdで描画していますが、拗促音(ゃゅょっ)や括弧は縦組み用の字形に切り替える必要があり、これは@espresso3389さんが作った(というかこのために作ってもらった)WaterTrans.TypeLoaderを使用していて、ここでstringをGlyphIdに変換しています。

//縦組み用の文字に変換している
static ushort[] stringToVerticalGlyphs(string text, SKPaint paint
  , WaterTrans.TypeLoader.TypefaceInfo typefaceInfo)
{
  ushort[] glyphs;
  paint.Typeface.CharsToGlyphs(text, out glyphs);
  var conv = typefaceInfo.GetVerticalGlyphConverter();
  for (int i = 0; i < glyphs.Length; i++)
  {
    if (conv.CanConvert(glyphs[i]))
      glyphs[i] = conv.Convert(glyphs[i]);
  }
  return glyphs;
}

XAML

作成したTategumiViewをXAMLで配置してます。WidthやHeightをBindingしていてViewの向きが変わると再処理(Compose)→描画(View)処理がおこなわれます。

<local:TategumiView VerticalOptions="FillAndExpand"
 HorizontalOptions="FillAndExpand"
 Width="{Binding TateviewWidth.Value}"
 Height="{Binding TateviewHeight.Value}"
 CurrentPage="{Binding CurrentPage.Value}"
 PageIndex="{Binding Path=PageIndex.Value,Mode=TwoWay}">

データ形式

HTML形式で<p>タグと<ruby>タグを読み込むようにしています。タグのパースはHtmlAgilityPackを使用しています。

<p><ruby>私<rt>わたくし</rt></ruby>はその人を...</p>

サンプルデータ

青空文庫から直接スクレイピングしようとしたのですが、文字コードやタグの使い方が統一されていなかったり(HtmlAgilityPackのPCLではXPATHが使えなかったり)できれいに取得できなかったので、元のHTMLからルビと本文だけのHTMLに加工しました。作成したHTMLリソースフォルダ(AndroidのAssetフォルダ、iOSのResoucesそれぞれ)に置いて読み込む形になっています。

表示!!

目次から書名をタップするとこんな感じで表示されます。左右のタップでページ移動します。

f:id:masatoru:20161220184545p:plain

GitHub

というわけで(不十分なところが多々ありますが)今回の縦組みサンプルをGitHubに上げました(現在Android版のみ動作します。iOSでなぜか動かないので調査中です)。奇特な方々の優しいダメだしを頂ければ大変幸いです。

最後に

今年もXamarinおよびXamariの関係者の方々と楽しく時間を過ごせました。ありがとうございました。来年もより一層よろしくお願いします。よいお年を。