友人のデザイナー、村上氏(@ryuki_kyoto)が好きなアプリのマイクロインタラクションの紹介と、それのサンプルを実装してみました。
紹介編
ピンを保存するボードリストをホールドした時、画像が右にずれ、文字間が詰まる事でユーザにフィードバックを返しています。
リストの一行が一枚の幕になっており、その膜に指を置いたことで文字が収縮したような感覚になります。
ユーザに対してより現実に近い表現のフィードバックを与えることで、UIと現実の境目が少しだけ小さくなっています。(by 村上氏)
Pinterest
レシピやインテリア、ファッションコーデなど試したくなるアイデアを発見しましょう。
Lifesum
ボタンを押すと、そのボタンが画面全体に広がり次の画面に遷移します。
自分の行動によって画面全体に影響が及ぶ爽快感や、そのボタンを押したことで自分がアプリの中に入っていくような没入感で、アプリのオンボーディングにおけるユーザのモチベーションを向上させます。(by 村上氏)
Lifesum Health App – Get Healthy & Lose Weight – Lifesum
実装編
PinterestとLifesumのインタラクションを参考に 「フィードバックを返すボタン」 と 「画面全体に広がり画面遷移をするボタン」のサンプルを実装してみました。
よりスマートな書き方があると思うのでぜひコメントで教えてください!
フィードバックを返すボタン
UIButtonクラスを継承してCustomButtonクラスを作成。
touchDown 時に、サムネイル画像が左にズレる & 文字間隔が詰まるアニメーションを発火させています。
import UIKit
class CustomButton: UIButton {
private var width: CGFloat = 0
private var height: CGFloat = 0
private var margin: CGFloat = 8
private var offset: CGFloat = 2
private var thumbnail: UIImageView!
private var label: UILabel!
private var tapping: Bool = false
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.masksToBounds = false
layer.cornerRadius = 4
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 4;
layer.shadowOpacity = 0.4;
backgroundColor = UIColor.init(named: "unTap")
thumbnail = UIImageView()
thumbnail.image = UIImage(named: "icon")
thumbnail.layer.masksToBounds = false
thumbnail.layer.cornerRadius = 4
addSubview(thumbnail)
label = UILabel()
label.text = "Snorlax"
label.textAlignment = .right
label.textColor = .gray
label.font = UIFont.systemFont(ofSize: 40)
addSubview(label)
}
override func layoutSubviews() {
width = frame.width
height = frame.height
if tapping {
onTapPosition()
} else {
unTapPosition()
}
}
func unTapPosition() {
let thmbnailSize = height - margin * 2
thumbnail.frame = CGRect(x: margin, y: (height - thmbnailSize) / 2, width: thmbnailSize, height: thmbnailSize)
let KernAttr = [NSAttributedString.Key.kern: 4]
label.attributedText = NSMutableAttributedString(string: label.text!, attributes: KernAttr)
let labelWidth = label.sizeThatFits(CGSize()).width
label.frame = CGRect(x: height, y: 0, width: labelWidth, height: height)
}
func onTapPosition() {
let thmbnailSize = height - (margin * 2)
thumbnail.frame = CGRect(x: margin + offset, y: (height - thmbnailSize) / 2, width: thmbnailSize, height: thmbnailSize)
let KernAttr = [NSAttributedString.Key.kern: 3.8]
label.attributedText = NSMutableAttributedString(string: label.text!, attributes: KernAttr)
let labelWidth = label.sizeThatFits(CGSize()).width
label.frame = CGRect(x: height + offset, y: 0, width: labelWidth, height: height)
}
func unTap() {
backgroundColor = UIColor.init(named: "unTap")
tapping = false
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveLinear], animations: {
self.unTapPosition()
}, completion: nil)
}
func onTap() {
backgroundColor = UIColor.init(named: "onTap")
tapping = true
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveLinear], animations: {
self.onTapPosition()
}, completion: nil)
}
}
import UIKit
class ViewController: UIViewController {
var button: CustomButton!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let width = view.frame.width
let height = view.frame.height
let buttonWidth = width * 0.8
let buttonHeight: CGFloat = 80
button = CustomButton()
button.addTarget(self, action: #selector(touchUpInside(_:)), for: UIControl.Event.touchUpInside)
button.addTarget(self, action: #selector(touchDown(_:)), for: UIControl.Event.touchDown)
button.addTarget(self, action: #selector(touchDragExit(_:)), for: UIControl.Event.touchDragExit)
button.setTitle("ボタンのテキスト", for: UIControl.State.normal)
button.setTitleColor(.red, for: UIControl.State.normal)
button.frame = CGRect(x: (width - buttonWidth) / 2, y: (height - buttonHeight) / 2, width: buttonWidth, height: buttonHeight)
view.addSubview(button)
}
@objc func touchDown(_ sender: UIButton) {
print("touchDown")
(sender as! CustomButton).onTap()
}
@objc func touchUpInside(_ sender: UIButton) {
print("touchUpInside")
(sender as! CustomButton).unTap()
}
@objc func touchDragExit(_ sender: UIButton) {
print("touchDragExit")
(sender as! CustomButton).unTap()
}
}
ボタンが広がって遷移するアニメーション
FirstViewController から SecondViewController への遷移時に下からボタンが現れるアニメーションが発火します。
ThirdViewController へ遷移時もフェードのアニメーションを付与しています。
import UIKit
class CustomButton: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(named: "emerald")
setTitleColor(UIColor.white, for: UIControl.State.normal)
titleLabel?.font = UIFont.systemFont(ofSize: 24)
layer.masksToBounds = false
layer.cornerRadius = 20.0
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
layer.shadowOpacity = 0.8
}
}
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navigationController: UINavigationController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window!.makeKeyAndVisible()
let firstViewController: FirstViewController? = FirstViewController()
navigationController = UINavigationController(rootViewController: firstViewController!)
navigationController?.setNavigationBarHidden(true, animated: false)
window!.rootViewController = navigationController
return true
}
// 略
}
import UIKit
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let buttonHeight: CGFloat = 60
let button = CustomButton()
button.frame.size = CGSize(width: view.frame.width * 0.6, height: buttonHeight)
button.addTarget(self, action: #selector(buttonTaped(sender:)), for: .touchUpInside)
button.setTitle("Push Me", for: UIControl.State.normal)
button.center = view.center
view.addSubview(button)
}
@objc func buttonTaped(sender: UIButton) {
navigationController?.pushViewController(SecondViewController(), animated: true)
}
}
import UIKit
class SecondViewController: UIViewController {
var width: CGFloat!
var height: CGFloat!
let buttonHeight: CGFloat = 60
let radius: CGFloat = 100
var button: CustomButton!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
width = view.frame.width
height = view.frame.height
//ボタンの生成
button = CustomButton()
button.frame.size = CGSize(width: width * 0.6, height: buttonHeight)
button.center.x = view.frame.width / 2
button.center.y = view.frame.height / 2 + 40
button.addTarget(self, action: #selector(cornerCircleButtonClicked(sender:)), for: .touchUpInside)
button.setTitle("Show Snorlax", for: UIControl.State.normal)
button.alpha = 0.3
button.isEnabled = false
view.addSubview(button)
// 遷移直後のアニメーション
UIView.animate(withDuration: 0.3, delay: 0.1, options: [.curveLinear], animations: {
self.button.isEnabled = true
self.button.alpha = 1.0
self.button.center = self.view.center
self.button.frame.size = CGSize(width: self.width * 0.6, height: self.buttonHeight)
}, completion: nil)
}
//角丸ボタンが押されたら呼ばれます
@objc func cornerCircleButtonClicked(sender: UIButton) {
button.setTitle("", for: .normal)
// 丸くするアニメーション
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveLinear], animations: {
self.button.layer.cornerRadius = self.radius / 2
self.button.frame.size = CGSize(width: self.radius, height: self.radius)
self.button.center = self.view.center
}, completion: { _ in
// 広がっていくアニメーション
UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveLinear], animations: {
self.button.layer.cornerRadius = self.height * 1.5 / 2
self.button.frame.size = CGSize(width: self.height * 1.5, height: self.height * 1.5)
self.button.center = self.view.center
}, completion: { _ in
self.pushViewController()
})
})
}
func pushViewController() {
// 画面遷移&アニメーション
let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
transition.type = CATransitionType.fade
self.navigationController?.view.layer.add(transition, forKey: nil)
let viewController = ThirdViewController()
self.navigationController?.pushViewController(viewController, animated: false)
}
}
import UIKit
class ThirdViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.frame = view.frame
imageView.image = UIImage(named: "icon")
view.addSubview(imageView)
}
}