Android版の写真の編集Viewをフルスクラッチしました

こんにちは、Androidエンジニアの瀬戸です。
今週リリースしたnohana AndroidのVer4.15.0でページの写真編集Viewのフルスクラッチを行いました。

過去の編集Viewの問題点

この編集Viewは3年半以上前にリリースしたVer3.0.0から使い回してきたのですが、動作として以下の問題点がありました

  • フレームレートが低く(10fps程度)、画像の移動などの体験が悪い
  • 動作が不安定になることがある
    • 画像の表示が遅い、画像が表示されない、稀に発生する強制終了etc..

内部的にも以下のような問題点がありました

  • パフォーマンス問題があることからわかるように、非効率な実装が多い
    • 無駄なインスタンス生成、ナイーブな描画
  • Viewであるにも関わらず、xmlで生成できない
  • IO処理があり メインスレッドで初期化できない
  • 責務の切り分けができておらず、各クラスの依存が…

弊社ではノハナ年賀状というアプリも出しているのですが、nohanaアプリの編集Viewは年賀状のアプリの編集Viewを参考に作られていました。
似たような機能を持ち、ちょっと差分のあるコードが別々に管理されている状況です。
メンテナンスに苦労していたため、このタイミングでフルスクラッチしてコードの共通化を行うことにしました。

プロジェクト国士無双

弊社のAndroidエンジニア3人の共通の趣味である日本酒から命名し、「国士無双」というプロジェクト名にしました。
国士無双には「比類なき国士」という意味があり、プロジェクトの高い目標としても良かったと思っています。
ちなみに国士無双を造っている高砂酒造さんは北海道の旭川にあり、とても良い日本酒を造っているので是非一度訪れてみてください。

要件

先ほどにも書いた通り2つのアプリから参照されるため、moduleとして切り出されている必要があります。

フォトブックアプリについては比較的シンプルな要件なのですが、年賀状アプリについては編集画面に多くの機能があり、以下のような要件が求められていました。
(アプリ側に実装が必要な機能もありますが、表示に関してはView側で対応する必要があります)

  • (年賀状のような)フレーム画像が表示
  • フレームの中に画像スロットを複数配置
  • 画像スロット
    • タッチイベントによる画像の移動、拡大縮小、回転
    • マスク画像によって画像のくり抜き
    • 画像にフィルター処理をかけられること
    • 画像スロットが単体でも利用できるようなViewであること
  • スタンプ
    • フレームへのスタンプの追加
    • タッチイベントによるスタンプの移動、拡大縮小、回転、削除
  • テキスト
    • フレームへのテキストの追加
    • テキストの右寄せ、左寄せなど
    • タッチイベントによるスタンプの移動、拡大縮小、削除
    • フォントの変更
    • 文字色の変更
    • 文字の縁取り色の変更
  • 編集完了後に画像として書き出し
    • 年賀状の入稿サイズとアプリ内で使うサムネイルサイズの2パターン
    • 当然、Viewの表示と書き出した画像に差異がないこと

煩雑にはなりますが、このような編集画面を実現し、プレビュー画像でも同じ画像を表示する必要があります

構造

編集Viewと画像スロットView

編集Viewの要件として「フレームの中に画像スロットを複数配置できること」というものがあります。
これを実現するために、編集ViewはViewGroupとして実装しました。
編集Viewにフレームの情報が設定されると画像スロットViewを動的に生成し、適切な位置にレイアウトしています。

ImageComposer

Viewの表示と同じ画像を書き出すためにImageComposerというクラスを作りました。
このクラスは編集状態を受け取ってCanvasに描画するだけのシンプルなクラスです。
Viewの描画は、編集状態をImageComposerに渡して処理を委譲しています。
画像生成の際には、編集状態をImageComposerに渡してBitmapから生成したCanvasに描画し、Bitmapを得ています。
すべての描画をImageComposer経由にすることにより、描画で差異が発生する可能性を減らしています。

実装

特に難しいことは行っておらず、当たり前のことを当たり前に実装してます。

タッチイベント

GestureDetectorを使い分け、それぞれの動作によって画像やスタンプなどを動かすようにしました。

  • タップ、ダブルタップはGestureDetector
  • ピンチイン・ピンチアウトはScaleGestureDetector
  • マルチタッチでの回転についてはRotateGestureDetector(自作クラス)

描画

各種端末の様々な解像度に対応できるよう、Canvas.scaleで描画領域を生成画像のサイズまで拡大縮小してから描画しています。これによって解像度を意識せずに描画を考えることができます。
画像スロットやスタンプの描画はMatrixを使って回転や拡大縮小を行っています。編集状態をMatrixに変換する処理はやや複雑ですが、変換を行うクラスを作って処理を意識させないようにしています。
マスク画像での画像くり抜きについてはPaint.xfermodeを使い、maskしない部分だけを描画するようにしています。

結果

機種によってブレがあるため具体的な数字は掲載しませんが、フォトブック、年賀状アプリについても触れば明らかに分かる程度には高速化することができました。
Kotlin化したことでコード的にもかなり量が減り、両アプリに分散していた7000行を越えるコードが1500行程度まで減りました。

構想2年、実装5週間のような工程でしたが、新規Viewの作成と既存アプリのView置換まで完了でき非常にすがすがしい気持ちです。
年賀状アプリについてはリリースまでもう少し時間がかかりますが、是非試していただきたいと思います。

And we are hiring!!