こんばんは,モノトーンです。
今日は画像の注目して欲しいところ,いわゆる [Focus Point] を実装した UIImageView
を作ろうというお話です。割と簡単なことなのですが,Focus Point 自体,実装されているサービスは Mastodon ぐらいしか知りませんw
実際問題として,画像を中心に展開するコンテンツというのはそのまま全体を表示していることが多いですしね。ただ,絵の一部をピックアップして,例えばサムネでは顔を中心に,そしてタップすると全身になる,みたいなのは割と自然なシナリオだと思ってて,あってもいいのかなーと。
まあ,次から具体的に前提知識から解説していきたいと思います。
1. UIKit の UIImageView
の仕組み
そもそも UIView
は CALayer
という Core Animation の Layer を 1:1 で持っていることは UIKit を触ったことある方なら誰でもご存知だと思います。UIView で 1:1 になっている CALayer は frame なども同期されますね。
実は UIImageView
は CALayer
に UIImage
のもつ CGImage
をセットしているだけの簡単な構造だと思われます*1。
CALayer
は GPU ベースで動くと考えられ,いわゆる Metal ベースな実装をしていると考えられます。
それはさておき,画像をアスペクト比固定で表示するときに,contentMode
を使うことは iOS アプリを作る上で誰しもが通る道だと思います。
これ,実際の中身は CALayer
の機能を使っていることをご存知でしたか?
contentsGravity
ですね。ほぼ 1:1 に対応していることがドキュメント見ていただければわかるかと。
ということで,あくまで CALayer
の contents
に CGImage
がセットされているだけ,ということは理解できたと思います。
2. CALayer のプロパティー contentsRect
x, y それぞれ [0, 1] の範囲で表示領域を決めるものです。
例えば,画像の右下 1/4 を表示したい場合,(x, y) = (.5, .5), width = .5, height = .5
のように設定します。
感のいい人ならわかると思いますが,focus point
から表示したい領域を計算し,それをセットするだけで目的の機能が実装できるんですよね。お手軽です。
3. 実際の実装(簡易)
Mastodon の focus point は以下のドキュメントを直接参照していますw
まず focus point F(xf, yf) を [-1, 1] 空間から [0, 1] 空間に写像します。
あとは画像を表示する領域 (Wc, Hc)と画像のサイズ (W, H) から,画像の表示する幅 Wまたは高さ H(以後長さ L と呼びます)を [0, 1] 空間で表現します。
そして,始点 P を 0 以上になるように決めます。このとき画像の端になりすぎないように中心側に少し引っ張られるように調整します。
始点 P に長さ L を足したものが 1 以下になるように調整します。
こんな感じですね。詳しい計算式はコードを参照してください。そんなに難しいことはやってないです。
このコード,割り算の部分だけ SIMDKit に依存しています。一度に x, y 両方とも割り算するようになってるだけです。もし,SIMDKit 使いたくなければ分解するか,必要なコードだけ SIMDKit から移設してください。
SIMDKit/CGSize+Operators.swift at master · mntone/SIMDKit · GitHub
まとめ
UIKit とか AppKit をここ三ヶ月は重点的に触っているのですが,やはり,CALayer 部分がよくできているなーって印象です。
macOS でも動く MTImage っていうのを作っていたんですが,自前で draw すると window リサイズでカクカクになるのに対して,CALayer にセットした画像はスムーズにリサイズされて気持ちが良い現象が起こったので,CALayer ベースの実装にしていたりします。
まあ,とりあえず iPhone が気持ちいいのは CALayer が素晴らしいっていうことで。
*1:UIImageView 自体 9-patch などに対応はしているので,そのあたりのコードは複雑そうですが