Quantcast
Channel: UXを高めるマイクロインタラクション実装 byゆめみ② Advent Calendarの記事 - Qiita

デザイナーさんが好きなアプリのマイクロインタラクションを実装してみた

$
0
0

友人のデザイナー、村上氏(@ryuki_kyoto)が好きなアプリのマイクロインタラクションの紹介と、それのサンプルを実装してみました。

紹介編

Pinterest

ピンを保存するボードリストをホールドした時、画像が右にずれ、文字間が詰まる事でユーザにフィードバックを返しています。
リストの一行が一枚の幕になっており、その膜に指を置いたことで文字が収縮したような感覚になります。
ユーザに対してより現実に近い表現のフィードバックを与えることで、UIと現実の境目が少しだけ小さくなっています。(by 村上氏)

2019-1-4-pinterest.gif

Pinterest
レシピやインテリア、ファッションコーデなど試したくなるアイデアを発見しましょう。

Lifesum

ボタンを押すと、そのボタンが画面全体に広がり次の画面に遷移します。
自分の行動によって画面全体に影響が及ぶ爽快感や、そのボタンを押したことで自分がアプリの中に入っていくような没入感で、アプリのオンボーディングにおけるユーザのモチベーションを向上させます。(by 村上氏)

2019-1-1-lifesum.gif

Lifesum Health App – Get Healthy & Lose Weight – Lifesum

実装編

PinterestとLifesumのインタラクションを参考に 「フィードバックを返すボタン」 と 「画面全体に広がり画面遷移をするボタン」のサンプルを実装してみました。
よりスマートな書き方があると思うのでぜひコメントで教えてください! :bow:

フィードバックを返すボタン

UIButtonクラスを継承してCustomButtonクラスを作成。
touchDown 時に、サムネイル画像が左にズレる & 文字間隔が詰まるアニメーションを発火させています。

2019-1-1-kunyu.gif

CustomButton.swift
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)
    }
}
UIViewController.swift
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 へ遷移時もフェードのアニメーションを付与しています。

2019-1-1-seni.gif

CustomButton.swift
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
    }
}
AppDelegate.swift
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
    }
    // 略
}
FirstViewController.swift
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)
    }
}
SecondViewController.swift
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)
    }
}
ThirdViewController.swift
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)
    }
}

React Nativeで実装する気持ち良いタブバーアニメーション

$
0
0

こんにちは。原健太です (@hellokenta_ja)

自己紹介

現在Standard Cognition (https://standard.ai) という会社でReact Nativeでアプリ開発をしています。
サンフランシスコに本社を置く「無人コンビニ」を作っている会社で、今はビザが取れるまで日本の支社で働いています。
しばらく日本にいるので、気軽にご飯とか行きましょう。

気持ち良いボトムタブバー

皆さん、ボトムタブバーって知ってますよね?
皆さんが何気なく、当たり前のように実装しているタブバー。工夫の余地が無いと思われがちなタブバー。
しかし僕は、Spotifyのアプリを触ったときに衝撃を受けました。まるで「生きている様だ」と。(ちなみにTwitterアプリとかもこんな感じです)

Apple Musicのタブバー(通常のタブバー)

普通の極みですね。
apple-music-tabbar.gif

Spotifyのタブバー

ぷにゅぷにゅしていて可愛い!!!何度でも触りたくなりますね!
spotify-bottom-tabbar.gif

雑談

ちなみに僕はSpotifyのアプリが大好きで、通勤時には「通勤におすすめの曲」が表示され、雨の日には「雨の日に聞きたいプレイリスト」を表示してくれる。
そんな、僕に寄り添ってくれるSpotifyアプリが好きなのです。

ソースコード

Spotifyの宣伝みたいになってきましたが、僕はSpotifyとは一切関係ありません。
そろそろソースコードを共有します。まず下記がタブバーのそれぞれのボタンを表すコンポーネントです。
こちらでぷにゅぷにゅアニメーションを実現しています。
*このままのコードで動くわけではありません。また、react-navigationのライブラリを利用しています。

tabbar-button.js
class TabbarButton extends React.Component {
  onPress = () => {
    const {
      navigation,
      route: { key },
    } = this.props
    navigation.navigate(key)
  }

  onPressIn = () => {
    Animated.timing(this.scale, {
      toValue: 0.95,
      duration: 40,
      useNativeDriver,
    }).start()
  }

  onPressOut = () => {
    Animated.spring(this.scale, {
      toValue: 1,
      friction: 0,
      useNativeDriver,
      overshootClamping: true,
    }).start()
  }

  scale = new Animated.Value(1)

  render() {
    const {
      currentIndex,
      routeIndex,
    } = this.props

    const focused = currentIndex === routeIndex
    return (
      <TouchableOpacity
        activeOpacity={0.8}
        onPress={this.onPress}
        onPressIn={this.onPressIn}
        onPressOut={this.onPressOut}
        style={{ flex: 1 }}
      >
        <Animated.View
          style={[styles.tabBarButton, { transform: [{ scale: this.scale }] }]}
        >
          <Icon color={focused ? black : gray} />
        </Animated.View>
      </TouchableOpacity>
    )
  }
}

ポイント

  • TouchableOpacityコンポーネントのonPressIn, OnPressOutを利用する。
  • Animated.springを利用してぷにょぷにょ感をだす。timingよりspringの方が個人的には気持ちよかったです。
  • overshootClampingtrueにしてバウンスしないようにする。
  • useNativeDrivertrueにすることによって、ネイティブのUIスレッドでアニメーションを動かす事ができ、パフォーマンスが向上します。毎フレームごとに状態をブリッジしなくて済む。→参考URL

その他コード

あとは上記で作成したタブバーのボタンを表示するだけです。参考までに他のコードも載せておきます。

tabbar.js
export default function Tabbar({ navigation, renderIcon, getLabelText }) => {
  const state = navigation.state
  const routes = state.routes

  const currentIndex = state.index

  return (
    <SafeAreaView style={styles.container}>
      {routes.map((route, routeIndex) => (
        <TabBarButton
          key={route.key}
          route={route}
          routeIndex={routeIndex}
          navigation={navigation}
          renderIcon={renderIcon}
          currentIndex={currentIndex}
        />
      ))}
    </SafeAreaView>
  )
}
routes.js
import Tabbar from 'components/tabbar'
const Tabs = createBottomTabNavigator(
  {
    Shopping: {
      screen: Shopping,
    },
    Receipts: {
      screen: Receipts,
    },
    Settings: {
      screen: Settings,
    },
  },
  {
    initialRouteName: 'Shopping',
    tabBarComponent: Tabbar,
  }
)

著者

Webサイトとマイクロインタラクション

$
0
0

初めに

この記事は「UXを高めるマイクロインタラクション実装 byゆめみ② Advent Calendar 2018」の20日目の記事です。
(投稿したのにリンクを忘れていました)

  • マイクロインタラクションとは?
  • Webサイトにおけるマイクロインタラクション
  • 考えるケースごとの理解
  • 実装例など
  • あとがき

この記事では上記の内容についての私見を書いていきます。
今回が初めてのアドベントカレンダー投稿になります。
皆さまの役に立つ記事であってほしいあわよくばMVPを掴み、MBPが欲しいという気持ちで書かれた記事になります。

記事の内容がぶれることを防ぐためにも自身が記事を書く際に置いた前提条件を次のセクション
「マイクロインタラクションとは?」
に書いておきます。

マイクロインタラクションとは?

ユーザビリティやUXを向上するために設計されたユーザーアクションに対する細かくも適切なフィードバックを指す。
主に下記のように設計、実装・実現される。

  • 細かなアニメーションや音、動作などでアクションに対するステータスを伝える
  • アクションに対して、その結果や次に操作するべきものへ視線を誘導するアニメーションなど

これらはユーザビリティの向上やそこから得られるユーザー体験(UX)の向上を目的にしたもの。
UXデザイン・設計における1要素であり、考え方、概念、用語。

この記事では上記を基にして「Webサイトにおけるマイクロインタラクション」について書いていきます。

Webサイトにおけるマイクロインタラクション

どのような場面でマイクロインタラクションの必要性や考え方が出てくるのか考えてみます。
前提としてどういったところにアクションが存在し、それに対するレスポンス、フィードバックが必要でしょうか?

ユーザーアクションへのレスポンスであることからクリックやタップにより押されたときや、マウスホバーに対する設計・実装が主ではないでしょうか。更にユーザーアクションということであればスクロールに対するレスポンスも少なからず存在するはずです。
また近年ではUIの実装としてフリック操作を用いたものも一般化されつつあり、マウスでの疑似フリック(ドラッグ)に対応したものも少なくありません。このことからフリックに対するマイクロインタラクションという実装幅が増えており、モバイルファーストの考え方もあることから、今後も増加していくでしょう。現状Webで見かけることは少ないと思いますが音声入力に対するマイクロインタラクションなどもその内議論されることになるように思います。

すぐに思いつくものだけでも下記のようなUI、ケースでマイクロインタラクションの考え方が活きてくることになります。

  • 操作可能なUIへマウスホバーしたとき
  • ハンバーガーメニューのアイコン変化やメニュー展開アニメ―ション
  • タブやアコーディオンUIの展開アニメーション
  • スライダーでPREV、NEXTが押されたときのボタンに対するアニメーション
  • スライダーでアクティブになったスライドの強調表示
  • ページローディングのプログレスアニメーション
  • ページ全体に対する現在のスクロール位置をバーのように表現するアニメーション
  • ラジオボタンやチェックボックスなどが押されたときの状態遷移アニメーション
  • フォームの入力の完了、残項目数を表す
  • 現在の入力文字数の変化を表す
  • お問い合わせフォームでの適切なフィードバック
  • ToDoやショッピングアプリなどで何かの追加、削除をわかりやすく示す

考えるケースごとの理解

上記で挙げた中からいくつかピックアップし、簡単にではありますが説明、紹介を行います。

「操作可能なUIへマウスホバーしたとき」

アニメーションなどを付ける頻度としてはこれが一番多いと思います。リンクやボタンなどクリック、ドラッグ可能であることを示すなど、普段当たり前に目にしているこれらもマイクロインタラクションといえるでしょう。

考えられるマイクロインタラクション

  • リンクにホバーしたときに下線が消える(または表示される)
  • ボタンにホバーしたとき操作可能であること、または操作できたことを示すアニメーションなど
考察・補足

ユーザビリティを考えると最低限何かしらの変化によって操作できることは示すべきです。
ボタンなどに対するUIパーツの移動や縮小を伴うアニメーションはボタンと背景の境界で思わぬ動作を引き起こすことがあるので実装時に注意する必要があります。
(移動などでホバーが外れ、連続してホバーとホバー解除を繰り返し、点滅するような動作に……)

「ハンバーガーメニューのアイコン変化、メニュー展開アニメ―ション」

ハンバーガーメニューは一般的に3本の横線で構成されるアイコンとそれが押されたときに展開されるメニューです。スマートフォンに向けたUIとして有名だと思いますが最近ではPCサイトでもハンバーガーメニューをナビゲーションに用いているサイトも珍しくないと思います。
アクションによるメニューの展開に合わせて、アイコンが変形し、「閉じる、戻る」アクションを示すアイコンに変化するよう実装されることが多いでしょう。アプリ、Webサイト問わず見かける既に一般化されたUIパーツだといえるのではないでしょうか。

考えられるマイクロインタラクション

  • 中央線が消えて、上下線を「×」になるようアニメーション
  • 3本線が「←」になるようアニメーション
  • アイコンを中心に背景、コンテンツが展開されていくアニメーションによりメニューへの画面遷移を明確にする
考察・補足

上記のようなアニメーションによりコンテンツ内容が切り替わり、メニューなどが展開されたこと、同じ箇所のクリックによって閉じる動作が行えること、行える動作が変わったことをユーザーに知らせる。
サイトの印象づけやUXのため、情緒的な演出に重きを置いているものもあります。ページ数が多いなど頻繁なページ遷移のあるサイトだとあまりに過度な演出は逆にUXを損なうことに繋がりかねません。効果的に使えば視線誘導や没入感の付与などUXの向上に繋げることができるでしょう。

「タブやアコーディオンUIの展開アニメーション」

タブやアコーディオンはページ遷移せず表示したいコンテンツ量が多い時や、カテゴリなどと紐づいたコンテンツを整理して表示したいときなどに用いられるUIパーツです。こちらに関してはかなり以前から用いられているものだと思います。ある意味でアコーディオンメニューのベースとなる発想だと思います。
(非表示の要素をアクションをトリガーにして表示する)

  • タブはトリガーとなる要素が押されたときコンテンツ部を切り替えて表示するUI要素です。
  • それに対してアコーディオンはトリガーを押されたときに非表示にしている要素を表示するものとなります。
UI イメージ
タブ 重ね合わせている要素を切り替える
アコーディオン 折りたたんでいる要素を展開し表示する

考えられるマイクロインタラクション

タブ
  • 押されたタブのトリガーを変化させて、クロスフェードでコンテンツを切り替える(コンテンツ変化をユーザーに示す)
  • 現在のタブのトリガーから押されたトリガーへと下線などがアニメーションするようにしコンテンツ遷移をより明確に示す
アコーディオン
  • コンテンツの高さをアニメーションし、最大まで展開することでコンテンツへ視線を誘導する(必須要件に近い)
  • アコーディオンのトリガーに矢印などがあるのであれば、反転させることでもう一度押すと閉じることを示す
考察・補足

どちらも非表示にしていた要素を表示するUIのため、アニメーションなどで適切に視線誘導を行い、コンテンツの変化を明確にすることが必要となるでしょう。これはハンバーガーメニューでも共通です。
スマートフォンにおいてはコンテンツの掲載量の関係からアコーディオンが使用される頻度は高く、コンテンツの切り替え時にコンテンツ頭までスクロールする処理なども場合によっては必要になることが考えられます。
(新たに展開されたコンテンツ内容を見失うことを防ぐ)
(スクロール操作を強制するものはユーザーに混乱を招くこともあることに注意が必要)

「お問い合わせフォームでの適切なフィードバック」

操作に対して適切なフィードバックがないと不安や不信、最終的にサイトの離脱に繋がることになります。中でもお問い合わせフォームなどは顕著だと考えられます。項目を選択したり、文章を入力した苦労の後に送信できているのか結果が不明確であるというのは決定的な不信や徒労だったという印象に繋がりかねません。

考えられるマイクロインタラクション

  • 問題ないのであれば入力完了を示すように送信ボタンが押せることを明確に示すなど
  • 入力内容に問題があるのであれば、ユーザーの入力時に適した内容へ誘導するようにする(リアルタイムバリデーション)
  • 送信時にエラーが起きたのであれば、まだ送信できていないこと、どう行動すべきかを示す(解消手段や代替手段の提示)
考察・補足

ユーザビリティとしての補足になりますが前提として何をするUIなのか明確でなければいけない。
(ラジオボタンとチェックボックスを混同するようなUIは避けたい)

その上で入力内容に問題があるのであれば可能な限りポジティブに、どうすべきなのか入力内容を誘導する。

メッセージ
ネガティブ、不明瞭 「エラーがあります。」「不正な値です。」
何をどう改善すべきか明確 「パスワードには半角アルファベット大文字を1つ以上入れる必要があります。」「住所は必須項目です。」

実装例など

簡単なものですが実装例など掲載できればと思います。

シンプルなリンク

See the Pen Simple Link by Yuichi A (@cf_rit) on CodePen.

アニメーションつきボタン

See the Pen Ripple Effect Button by Yuichi A (@cf_rit) on CodePen.

ハンバーガーメニュー

See the Pen Simple Hamburger Menu by Yuichi A (@cf_rit) on CodePen.

あとがき

ユーザビリティやそこから派生してUXを考える際に操作したときのわかりやすさ、気持ちよさ、自然さは重要なものだと思います。UIであるなら最低限、操作できること、操作ができたことを示す必要があるでしょう。
ユーザーが使いやすいサイト、印象に残るサイトを作り、より良いUIアニメーションやUX設計などを考えていきたいですね。その中でマイクロインタラクションの考え方が活きてくることになると思います。

感想

初のアドベントカレンダー記事でしたが構成、校正ともに割く時間が足りなかったなと思います。実装例はフォーム関連やスクロールに合わせたインジケーターのようなものも実装し掲載したかったのですが業務やプライベートの事情で時間が作れず、投稿前の10-30分で書くことに。

また実際のサイトに乗せることを想定した案件レベルのコード掲載を予定していたのですが趣味のレベル、動きを再現するだけになってしまった点は大きな改善点として悔いが残ります。総じて書きたい内容をまとめきれなかったかもしれません。ですが記事を書くこと、投稿すること自体に意味はあると思いますので、こうして投稿させていただきます。
次回、記事を書く際にはもっと構成を考えた上、要点を絞って書く必要を感じた12月20日でした。

拙著ではありますが誰かのお役に立てば幸いです。

にわかに流行中な「徐々にHover効果」を実装する

$
0
0

通常のホバーは、「オン」と「オフ」のタイミングでのみアクションしますが、
ポインティングデバイス(と要素と)の距離や角度を監視すると、漸進的なマイクロインタラクションを作ることができます。

こういった機能自体は、割と昔からさりげなく使われていたりするかと思いますが、
今回は手軽に使えるように、proxemicsという名のlibraryを作成しました。

https://github.com/pokkur/proxemics

ポインターが画面左端に近づくほどサイドバーが引き出される

sidebar.gif

See the Pen Progressive Hover Effect Demo 03 by pokkur (@pokkur) on CodePen.

Proxemics('.side-bar', {
    territory: 0 // ポインターを検知するテリトリー(半径)
}, (_, Styles) => {
    // 水平距離でのみアクションさせるための変数
    const distanceX = _.distance * Math.max(0, Math.cos(_.radian))
    Styles({
        // サイドバーの横幅(33px)プラス若干分までポインターが水平に接近した時に、サイドバーを全開にさせたい
        transform: `translate(${Math.min(-distanceX + 50, 0)}px, 0)`
    })
})

タイル・ギャラリーのフォーカス表現

tile4.gif

See the Pen Progressive Hover Effect Demo 01 by pokkur (@pokkur) on CodePen.

Proxemics('.prox', {
    territory: 100
}, (_, Styles) => {
    // 対象要素の彩度をポインター距離でコントロール
    Styles({ filter: `saturate(${Math.max(1 - _.distance * .01, 0)})` })

    // ポインターが一定の範囲に近づいた時に、画像の拡大率を変化させる
    const Images = _.el.querySelectorAll('img')
    Array.prototype.forEach.call(Images, (Image) => {
        Image.style.transform = `scale(${Math.max(1, (1 - _.distance * .001) + .2)})`
    })
})

その他色々

https://pokkur.github.io/proxemics/

「徐々にHover効果」の使用範囲は当然デスクトップデバイスに限られてしまいますが、
アイデア次第で「わかりやすさ」、「つかいやすさ」などのUX向上を狙える新しめのアプローチのひとつです。
proxemicsをご利用の上、ぜひお試しください〜。

初めて出したiOSアプリのUI/UXの工夫点とその実装

$
0
0

概要

UXを高めるマイクロインタラクション実装 byゆめみ② Advent Calendar 2018の12/22(土)の記事です!!

業務でiOSを開発して一年以上経過し、この前自分で作ったアプリをリリースできました。(ネイティブ開発はAndroid, Unity, Cordovaなどでかれこれ5年ほどやっています)
そのアプリを開発する際にマイクロインタラクションで工夫した部分なぜそうしようとしたかを紹介いたします。

アプリの概要

ウェナタップ - いつタップしたかわかるアプリ

その名の通り、アイコンを並べてそのアイコンがいつタップされたかがわかります。アイコンのタップ回数などもわかるアプリです。

工夫点の紹介

Collection Viewのアイテムをタップしたときのインタラクション

問題

iOS標準のままのCollectionViewのCellではタップされたのかわからない

工夫点

一般的な解決方法ですが、タップしたcellのimageの部分をアニメーションさせてタップを表現しました。
このアプリはタップした時間を表示するアプリなので、imageの下の「何分前に最後にタップしたかの情報」と「最後にタップした時刻」が変化します。

問題その2

上のタップアニメーションなどだけのインタラクションだとユーザーはこのタップという行為にどういう意味があるのかわからない。

工夫点その2

タップアニメーションだけだとタップしたことしかわかりません。

タップという行為がどのようなことに繋がっているのかということを示したかったので、「今日のタップ回数」と、「今までのタップ回数」をタップした分だけ増やすことで、自分のタップは全て記録され、カウントされているのだということをユーザーに感じてもらえるようにしました。(下のInfo部分)

実装

    /// imageのアニメーション
    func animateImage(completion: (() -> Void)? = nil){
        UIView.animate(withDuration: 0.1, animations: {
            self.imageView.transform = CGAffineTransform.identity.scaledBy(x: 1.3, y: 1.3)
        }) { completed in
            self.imageView.transform = CGAffineTransform.identity
            completion?()
        }
    }

前の画面に戻ったときにTableのどこを選択していたか表現する

問題

遷移先の画面から戻ってきた時、自分がどこを設定したかがわからない。

工夫点

新規のタスクの作成画面です。アイコン選択ですが、戻ってきたときにアイコンを自分が選択したかがわかるようになっています。

実装

UITableViewControllerを利用しているなら何もしなくてもなります。
普通のTableViewの場合は以下です。

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        tableView.indexPathsForSelectedRows?.forEach{
            tableView.deselectRow(at: $0, animated: true)
        }
    }

スクロール可能領域が変化したときにバーとinsetを変化させる

問題

このアプリはCollectionViewと下から出てくるタップ数を記載したView(SubViewとこれから呼びます)の2つの構成です。
SubViewが出ると、CollectionViewのアイテムの下の部分がタップできなくなるといった問題や、表示領域が少なくなってしまうという問題があります。

工夫点

スクロール領域をSubViewに合わせて変化させ、最後のアイテムまでスクロールできるようにしました。

表示領域が狭いという問題に関しては、SubViewをガラス張りのView(iOS標準)にすることで、下にまだアイテムがあるということがわかるようにしました。

最近のiOSではiphoneXの登場もあり、画面いっぱいに要素を表示したいという思想があると思います。
今回のアプリでも、できるだけメインコンテンツのCollectionViewのアイテムをいっぱい見せるため、navigationBar,SubView,bottomBar全てで標準のガラス張りを採用しました。

実装

PropertyAnimator利用しています。こちら参考に実装しています。
Advanced Animations with UIKit

    ///下のViewのアニメーションに合わせてscrollIndecatorInsetsなどを変化させる。
    private func addCollectionViewInsetAnimator(state: State, duration: TimeInterval) {
        let insetAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1)  {
            switch state {
            case .expanded:
                self.collectionView.scrollIndicatorInsets.bottom = self.bottomMiddleHeight + 5
                self.collectionView.contentInset.bottom = self.bottomMiddleHeight + 5
            case .collapsed:
                self.collectionView.scrollIndicatorInsets.bottom = self.bottomHeaderHeight + 5
                self.collectionView.contentInset.bottom = self.bottomHeaderHeight + 5
            }
        }
        runningAnimators.append(insetAnimator)
    }

スクロールできるViewは見えたときにbarを表示

問題

そもそもスクロールできるかわからない。

工夫点

Viewが表示されたときに、スクロールバーを表示する。

3つ目のセルは横にスクロールしてたくさん色を選択できるようになっています。横スクロールできるという発想はなかなか起こらないと思うので、スクロールバーをflushさせることでスクロールできることをユーザーにアピールしています。

実装

ViewControllerのviewDidAppearで表示する

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        colorCollectionView.flashScrollIndicators()
    }

編集モードを明確に表現する

問題

編集モードになってもCollectionViewの場合はわかりにくい
(EditButtonの「編集」が「完了」に変わるだけ)

工夫点

あまり見ない方法だと思いますが、通常モードでは出していないnavigationBarを表示することで編集モードになったこと、なっていることがわかりやすいようにしました。

また、編集モードで何ができるかをpromptを利用して説明しています。

他には、以下が考えられると思います。
* アイコンの周りにバッジを表示したりして、編集できることを表現する
* barや背景色を変化させる

実装

    override func viewDidLoad() {
        super.viewDidLoad()
        toolbarItems?.insert(editButtonItem, at: 1) // editButton追加
        setEditing(false, animated: true)
    }

    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        navigationItem.prompt = editing ? "編集・削除したいアイテムをタップしてください。" : nil
        title = editing ? "編集モード" : nil
        navigationController?.setNavigationBarHidden(!editing, animated: true)
    }

UITextFieldを含むCellはCellのタップ時にテキスト入力できるようにする

問題

TableviewのCellのなかに配置しているTextFieldをタップしようとして、Cell自体をタップしてしまう。
すると、Cellが選択されるだけで終わってしまう。

工夫点

Cellがタップされた時も、CellのなかのTextFieldにフォーカスするようにする。
もちろんTextFieldがタップされればそのまま入力できます。

実装

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // テキスト入力のセルがタップされた
        if indexPath.row == 1 {
            titleTextField.becomeFirstResponder()
            tableView.deselectRow(at: indexPath, animated: true) //これやらないとずっと選択されたまま
        }
    }

最後に

初のiOSアプリの初のAdvent Calendarでしたがいかがでしたでしょうか。

自分はどのようなインタラクションを行うとユーザーがどう思うかということを考えるのが好きなのですが、マイクロインタラクションの事例をもっと知っていきたいです。

この記事が誰かの役に立てば幸いです。
以上です。ありがとうございました。

作ったアプリ

ウェナタップ - いつタップしたかわかるアプリ
まだタップした時間や回数がわかるだけのアプリですが、もっと生活に役立つアプリに進化させていきたいと思います!!
ご期待ください^^

参考にしたサイト

動的なSVGで、待たないインジケータをつくる

$
0
0

タイトルを意気込んで書いたので、まずは背景から。

インジケータは何のためのもの?

ユーザーとしてアプリケーションに触れる際は、インジケータはあたりまえすぎるものなので、何のため、というのは意識しないかもしれません。

ですが、エンジニアの方でしたら、1度くらい、経験したことがあるはずです、開発途中のインジケータ未実装のアプリケーションを。

インジケータが未実装だと、どうなるのか?というと、

  • ユーザーのアクションに対するリアクションがなく、シーン...となるので、システムが「何もしていないように見える」
  • そして、ユーザーが何かミスったかなと思い、「連打する」
  • しかし何も起こらないので、結果ユーザーは「壊れたと勘違いする」

こんなことが起こります。

インジケータっていうのはユーザーに対してのメッセージであり、コミュニケーションですよね、というところです。

続いて、インジケータがあるものの、それによって何を伝えるか、です。

インジケータはなんかぐるぐるしているものをとりあえず置いておけばいいんじゃないの?

という考え方もあると思います。

ですが、インジケータの役割は、

  • 待ってもらうためのもの

であり、そうなると、単にインジケータを置いても、「ユーザーを待たせている」わけです。

であれば、何かしらで、その心理的負担を取り除いてあげたいですよね?

たとえば

  • いつまで待てばよいのかわかる
  • できたらお知らせしてくれる
  • 待っていると感じさせない

など。

今回は、この中でも、「待っていると感じさせない」をやってみたいとおもいます。

成果物

janken.gif

これは何をしているのかというと、ユーザーのクリックする、というのに対してリアクションするSVG画像です。

・・・インジケータ、、、ではないですねもはや笑

しかもなんか「はじめてのアプリケーション」みたいになってしまいましたね、、、、

しかし、意図はあって。

一般的なインジケータだと、「待つ」という行いは絶対発生するが、こういったように、待つのではなく、
他の目的を与えることで、待たないインジケータになりうるという話です。

さらに言うと、それを動的なSVGで実装してみようよ、という話です。

実際、画像やCSS、アニgifなどはあっても、

  • ドットの集まりではなく、定義に基づいて描画ができる表現力豊か
  • 動的に振る舞いを変えられる

というのは、SVGをうまく活かせるところなのでは!

技術的な話

さて、ここで技術的な話ですが、
いきなりですが、インタラクティブな部分はJSを使ってます。
なんだよって感じですが。

ただ、SVGって面白くて、JSでアクセスしてアレコレできてしまうんですよね、、
SVGの一番外側だけではなく、SVGの内部に、ですよ。
SVGの中の要素にアクセスできてしまうんです。

しかも、SVGにSVGを入れ子したりもできるし、JS自体もSVG内に書くことができます。
そのあたりのパッケージ感が面白いです。

それを踏まえて、上記添付のSVGに関しては、ぼくの拙い絵をSVGで描画したもの複数個を、親のSVGに入れ子にしてあります。
さらに、同じく親のSVGの入れ子のJSで制御しています。

日本語的に何を言っているのかわからなくなる頃だと思うので、イメージはこんなかんじです↓

<!-- イメージ -->
<svg>
  <svg></svg>
  <svg></svg>
  <svg></svg>
  <script></script>
</svg>

これを使うこなすと、やろうと思えば激しくリッチにできてしまうんですよね。

そして何かに似てるなぁ、、と思ったところ、キャラ的にはかつてのFlashに似ていますよね。

なので、そういった目線でSVGを見ると変わって見えるかもしれません。

SVGって、なんかなめらかに描ける画像みたいなアレでしょ、という風に捉えている方も少なくないと思いますが、こうやって技術に基づいて色々見ていくと、今までできなかった マイクロインタラクション なども、ちょっと違うアプローチで解決したりできるようになるのではないでしょうか??

色々試してみてください!!

雑然としてますが、ソースコードはこちら