綴Photo の詳細画面を作るとき、 「写真の下からそのままテキストが続く」体験にこだわった。 紙のノートに写真を貼って、その下に文字を書き連ねるイメージだ。 言葉にすると当たり前なんだけど、コードで作ろうとすると地味にハマる場所があった。
なぜ罫線ノートにしたかったか
綴Photoの詳細画面は「思い出を綴るページ」という位置づけにしています。 だから設計の最初から、ノートのメタファーを強く意識していた。
写真の下に罫線が続いていて、その上にテキストが乗っています。 紙のノートに写真を貼ってから、その下に書き始める——あの感覚をそのままデジタルに持ち込みたかった。
普通に罫線を引くだけならそんなに難しくないです。
Container の Border を並べてもいいし、背景に薄いリピート画像を置いてもいいです。
でも実際にやってみると、ある違和感に気づいた。
詰まったところ:罫線の「位相」が合わない
問題はこれだった。
写真エリアとテキストエリアで、罫線の間隔(行の高さ)を同じにしているのに、 写真エリア下端からテキスト1行目までの距離が、罫線の間隔とぴったり合わない。
たとえば行間が 28px だとして、写真下端からテキスト1行目までが 35px だったとする。
見た目は些細な違いなのに、「ノートの上に写真を貼った」感が消える。
ここで「あ、罫線は単に並べればいいわけじゃない。位相が合っている必要があるんだ」と気づいた。
最初は罫線間隔とパディングをミリ単位でいじって調整しようとしたんだけど、 画面幅やフォントサイズが変わると一発でズレます。 「数字の調整で解決する問題ではないな」と途中で諦めた。
CustomPainter で startY を渡して位相を揃える
解決策は意外とシンプルで、写真エリアとテキストエリアの両方で 同じ間隔・同じ位相で罫線を描くだけだった。
ただ、それを実現するには「テキストエリアの罫線を、どのYから引き始めるか」を、 写真エリアの最終ライン位置から逆算する必要があります。
そこで CustomPainter で罫線を引くウィジェットに startY を渡せるようにした。
class RuledLinePainter extends CustomPainter {
final double startY; // 最初の罫線をどのYから引き始めるか(テキスト側は写真エリアの最後のYを渡すと位相が繋がる)
final double lineGap; // 罫線の間隔(行の高さ)
final Color lineColor;
@override
void paint(Canvas canvas, Size size) {
var y = startY;
while (y < size.height) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), _paint);
y += lineGap;
}
}
}コードで再現できた瞬間だった。地味に嬉しかった。
この実装で意識したこと
罫線そのものはシンプルなロジックなんだけど、UI として馴染ませるために細かい調整が要った。
- 罫線の色:濃すぎると主張が強くなります。実際にはかなり薄いグレーにしています
- 線の太さ:
strokeWidth = 1.0の固定です。細い線なので、DPI ごとに変える調整までは入れていません - 行間:本文のフォントサイズに合わせて変えます(後にデザイントークン化した。これは別記事に分けたい)
数字の細かい話より、「現実のノートで起きていることをコードに落とす」という発想が大事だった気がする。
紙のノートを観察すると、線の濃さは紙の色を邪魔しない範囲にあります。 これを言語化してコードに落とすと、迷いがほとんどなくなった。
学んだこと
- 罫線 UI は「並べる」だけじゃなく「位相を合わせる」ことが大事です
- CustomPainter は最初の一歩を踏み出せば、思ったほど怖くないです
- 「紙ノートで起きていることをコードで再現する」と考えると、ロジックは案外シンプルになります
- 数字を直接いじるより、計算でズレを吸収できる構造にする方が、後から壊れにくいです
紙のメタファーをコードに落とし込むとき、現実の物理がヒントになります。 罫線の話に限らず、テープを貼る・付箋をめくる・ページが少しズレる、 そういう「現実だと当たり前」のことをコードで言語化していく作業は、地味に楽しいです。
綴Photo はこのあと、写真カードの傾きや付箋の偶奇ズレなんかも入れていくんだけど、 全部「紙でやってることをコードに翻訳する」という同じ発想の延長にあった。
この記事は2026年4月時点の自分の理解と実装方針で書いています。アプリは継続開発中なので、現在の実装と細部が違っている可能性があります。