綴Photo の「写真の見せ方」を直した記録。デスクトップで一番大きく出す写真が縦に潰れていた原因と、写真メインのアプリで「枠に収める」より「全部見せる」をどう選んだか。
デスクトップ版の home 画面は、左半分に「最近綴った記憶」の写真を大きく出しています。使っていて、その写真が縦に間延びして見えることに気づいた。横長の風景写真が、ちょっとぽっちゃりして見える。最初は表示の BoxFit(写真を枠にどう収めるかの表示設定)の問題かと思ったけど、そこは cover(比率を保って枠を埋める=はみ出た端は切れる)になっていて、それなら歪まないはずだった。
原因は、枠のサイズをそのままデコードに渡していたこと
AI にコードを追ってもらったら、犯人は表示じゃなくデコードのほうだった。画像を読み込むとき、メモリ節約のために「このサイズで読み込んで」と指定しています。home パネルはそこに Size(w, w)=正方形を渡していた。
// 横長写真を正方形サイズでデコード=この時点で縦に潰れる。
// 表示の cover は「潰れた正方形」を枠に合わせるだけで、もう手遅れ。
decodeLogicalSize: Size(w, w), // ← 犯人なぜ他の画面は無事だったのか
気になったのは、詳細画面も同じ部品で写真を出しているのに、そっちは潰れていないことだった。
ここで原因が腑に落ちた。本棚のサムネは見せ方(cover)を変えず、デコードサイズだけ一緒に直した。
比率を保つサイズを計算する
直す方向は決まった。デコードに渡すサイズを、元写真の比率を保ったまま枠を覆うサイズにする。これは純粋な計算なので、AI にテストから先に書いてもらった(テストを書いて、失敗を確認してから実装する流れ)。
// cover 表示で枠を覆う、元写真の比率を保ったデコードサイズ。
Size? coverDecodeLogicalSize(Size frame, double? aspectRatio) {
if (aspectRatio == null || aspectRatio <= 0) return null;
final frameRatio = frame.width / frame.height;
// 写真が枠より横長なら高さを枠に合わせる(幅がはみ出てクロップ)。
return aspectRatio > frameRatio
? Size(frame.height * aspectRatio, frame.height)
: Size(frame.width, frame.width / aspectRatio);
}純関数なので「横長×正方形枠なら Size(200,100) が返る」みたいなテストで挙動を固められます。
同じ理屈で、枠に収める側(contain 用)も対で作った。並べてみると、条件式は同じで返す枝だけが入れ替わっています。
// cover = 足りなくなる側の軸を枠にぴったり合わせる(もう片方がはみ出てクロップ)
return aspectRatio > frameRatio
? Size(frame.height * aspectRatio, frame.height) // 枠より横長 → 高さを合わせる
: Size(frame.width, frame.width / aspectRatio); // 枠より縦長 → 幅を合わせる
// contain = はみ出す側の軸を枠にぴったり合わせる(もう片方に余白が出る)
return aspectRatio > frameRatio
? Size(frame.width, frame.width / aspectRatio) // 枠より横長 → 幅を合わせる
: Size(frame.height * aspectRatio, frame.height); // 枠より縦長 → 高さを合わせる短い関数なのに、読むたびに「どっちがどっちだっけ」で一瞬止まります。条件の aspectRatio > frameRatio も「写真が横長か」ではなく「写真が枠より横長か」で、縦長写真でももっと縦長の枠に入れれば成立します。
対で作るとき片方から写すと、そのまま真逆になりえます。厄介なのは、取り違えても返る比率は正しいままなところです。潰れないので画面を見ても分かりません。cover 側を間違えれば枠を覆えず小さくてぼやける、contain 側なら必要以上に大きくデコードしてメモリを余分に食う——絵は合っているのに数字が違う、という出方になります。
本棚は cover、home は「全部見せる」contain
ここからが判断だった。潰れを消すだけなら、全部 cover(比率を保ってクロップ)で揃えてもいい。でも実機で見ていて、home の大きな写真パネルは違う気がした。綴Photo は写真に言葉を綴るアプリで、写真が主役です。cover は枠を埋めるかわりに写真の端を切ります。本棚みたいな小さいサムネは、整列のためにクロップでいい。でも home のメインの写真は、切らずに全部見せたいと思った。
なので home だけ contain(写真全体を枠に収める)に変えると決めた。問題は、縦長のパネルに横長写真を全体表示すると上下に余白が出ること。そこは、同じ写真を大きくぼかして背面に敷いて埋めることにした(Instagram の縦長投稿でよく見るやつ)。
// 背面: 同じ写真を cover で広げてぼかし、余白を埋める。
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: AppPhotoImage(source: photo, fit: BoxFit.cover /* ... */),
),
// 前面: 写真全体を contain で見せる(主役)。
AppPhotoImage(source: photo, fit: BoxFit.contain /* ... */),実機で値を詰める
ぼかしの強さと、背面を暗くする量は理屈で決まらないので、実機で見ながら動かした。最初はぼかし強め・暗め(sigma 28・黒 38%)にしたら、写真の世界観が背面で死んでいた。段階的に弱め・明るめへ落として(ぼかし sigma 28→12、背面の黒 38%→18%)、前面の写真がちゃんと主役で、背面はうっすら色が回る程度に落ち着いた。この good / no-good は実機を見ないと判断できなかった。
学びポイント
- 表示の
fitが正しくても、デコードのサイズ指定で比率は壊れます。 幅・高さ両方を枠サイズで渡すと exact policy で潰れます。デコードに渡すサイズは「元写真の比率を保った値」にします。 - cover と contain は、同じ条件式で返す枝が入れ替わっているだけ。 取り違えても比率は正しいまま返るので、画面を見ても気づけません(枠を覆えず小さい/必要以上に大きい、という形で出ます)。
- 同じバグでも、画面ごとに出たり出なかったりします。 無事な画面との違い(デコードに渡すサイズが元比率か枠比率か)を並べると原因が絞れた。表示の cover/contain は関係なく、効いていたのはデコードサイズのほうだった。
- 「潰れを直す」と「どう見せたいか」は別の判断。 整列させるサムネは cover、主役の写真は contain。アプリが何メインかで選びます。
- 理屈で決まらない値(ぼかし・暗さ)は実機で動かします。 コードは AI が書けても、good / no-good の最終判定は自分の目だった。
おわりに
写真メインのアプリなのに、一番大きく出す写真が潰れて端も切れていた。直してみて、「枠に合わせる」のが当たり前じゃなく、「全部見せる」を選べる場面があると分かった。小さく整列させる所はクロップ、主役は全体表示。同じ写真の出し方でも、場所によって判断が違っていいです。
この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。