みかづきブログ その3

3ヶ月つづけてみました。

昨今どんどん巨大化するスマートフォンの為のUIを考えた結果、ななめにスクロールするUIもいいんじゃないかと思いました

きっかけは この記事 を読んだことにあります。
(もう1年以上前の記事なんですね)

gigazine.net

いろいろと参考になることが書いてあるのですが、なかでも一番気になったポイントは右手でスマートフォンを操作する人の指の届く範囲を表したこの図でした。

http://i.gzn.jp/img/2014/12/09/ios-android-ux-design/a19_m.png
http://gigazine.net/news/20141209-ios-android-ux-design/


画面上に、あきらかに押しやすい箇所と押しにくい箇所があります。
であればいっそ、押しやすい箇所を操作範囲としたUIをつくればいいんじゃなかろうか。
そんなことを思ってつくったモックがこちらです。

codepen.io

PCだとちょっと操作しにくいので、是非ともiPhone6以上のサイズのスマートフォンでご観覧頂きたいです。
(iPhoneでしか確認してないのでAndroidでは動かないかもしれません)

f:id:kimizuka:20160121084928g:plain
http://codepen.io/kimmy/full/LGOaww/

操作範囲を斜めにしたことによって、いままでとはひと味違ったUIになったのではないでしょうか。

ソースコード

HTML
<div id="wrapper">
  <header id="header">
    <p class="txt">CARD UI</p>
  </header>
  <ol id="lists">
    <li class="list a0 prevback">
      <article class="article">
        <h2 class="ttl">A</h2>
        <p class="txt">Hello world.</p>
        <div class="img"></div>
        <p class="txt book">Hello world.</p>
      </article>
    </li>
    <li class="list a1 prev">
      <article class="article">
        <h2 class="ttl">B</h2>
        <p class="txt">Hello world.</p>
        <div class="img"></div>
        <p class="txt book">Hello world.</p>
      </article>
    </li>
    <li class="list a2 main">
      <article class="article">
        <h2 class="ttl">C</h2>
        <p class="txt">Hello world.</p>
        <div class="img"></div>
        <p class="txt book">Hello world.</p>
      </article>
    </li>
    <li class="list a3 next">
      <article class="article">
        <h2 class="ttl">D</h2>
        <p class="txt">Hello world.</p>
        <div class="img"></div>
        <p class="txt book">Hello world.</p>
      </article>
    </li>
    <li class="list a4 nextback">
      <article class="article">
        <h2 class="ttl">E</h2>
        <p class="txt">Hello world.</p>
        <div class="img"></div>
        <p class="txt book">Hello world.</p>
      </article>
    </li>
  </ol>
  <footer id="footer">
    <p class="txt">&copy; KIMIZUKA.FM</p>
  </footer>
  <div class="btn close"></div>
</div>
SCSS
$mainColor: #fefefe;
$subColor: #eee;
$bgColor: #666;
$time: 0.3;

* {
  -webkit-user-select: none;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

html {
  height: 100%;
}

body {
  position: relative;
  height: 100%;
  font: 16px HelveticaNeue-UltraLight, sans-serif;
  background: $subColor;
}

#wrapper {
  position: absolute;
  top: 0; bottom: 0;
  left: 0; right: 0;
  margin: auto;
  width: 320px; height: 460px;
  background: $bgColor;
  overflow: hidden;

  &:after {
    display: block;
    position: absolute;
    top: 0; bottom: 0;
    left: 0; right: 0;
    content: "";
    pointer-events: none;
    z-index: 9;
    box-shadow: 0 0 5px rgba(0, 0, 0, .5) inset;
  }
}

#header {
  position: absolute;
  top: 0; left: 0;
  width: 300px; height: 300px;
  background: $subColor;
  transform: rotate(-45deg) translateY(-250px);
  z-index: 10;
  box-shadow: 0 0 5px rgba(0, 0, 0, .5);

  .txt {
    position: absolute;
    bottom: 20px; left: 0;
    width: 100%; text-align: center;
  }
}

#lists {
  position: relative;
  width: 100%; height: 100%;
}

.list {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  transition: top #{$time}s ease-out, left #{$time}s ease-out, opacity #{$time * 2}s;

  .article {
    position: absolute;
    box-sizing: border-box;
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    background: $mainColor;
    overflow: hidden;
    transition: all #{$time}s ease-out;
    transform-style: preserve-3d;
    transform: rotateZ(-5deg);
    box-shadow: 0 0 2.5px rgba(0, 0, 0, .4);
    overflow: hidden;
    cursor: pointer;
  }

  .ttl {
    padding: 20px 20px 10px;
    font-size: 150%;
  }

  .txt {
    padding: 10px 20px;
  }

  .img {
    margin: 10px 0;
    left: 0; bottom: 0;
    width: 100%; height: 50%;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: cover;
  }

  &.prevback {
    top: -100%; left: 50%;
    opacity: 0;
    z-index: 0;

    .article {
      transform: rotateZ(40deg);
    }
  }

  &.prev {
    top: -60%; left: 30%;
    transition: top #{$time - 0.05}s ease-out, left #{$time - 0.05}s ease-out;
    z-index: 1;

    .article {
      transform: rotateZ(20deg);
      box-shadow: 0 0 2px rgba(0, 0, 0, .3);
    }

    .down & {
      transition: top #{$time + 0.1}s ease-out, left #{$time + 0.1}s ease-out;
    }
  }

  &.main {
    top: 0; left: 0;
    z-index: 2;
  }

  &.next {
    top: 55%; left: -25%;
    transition: top #{$time + 0.1}s ease-out, left #{$time + 0.1}s ease-out;
    z-index: 3;

    .article {
      transform: rotateZ(-20deg);
      box-shadow: 0 0 3px rgba(0, 0, 0, .2);
    }

    .down & {
      transition: top #{$time - 0.05}s ease-out, left #{$time - 0.05}s ease-out;
    }
  }

  &.nextback {
    top: 100%; left: -50%;
    opacity: 0;
    z-index: 0;

    .article {
      transform: rotateZ(-40deg);
    }
  }

  &.a0 {
    .img {
      background: #E57373;
    }
  }

  &.a1 {
    .img {
      background: #F06292;
    }
  }

  &.a2 {
    .img {
      background: #BA68C8;
    }
  }

  &.a3 {
    .img {
      background: #9575CD;
    }
  }

  &.a4 {
    .img {
      background: #7986CB;
    }
  }

  &.on {
    animation: on 1s ease-in-out;
    animation-fill-mode: forwards;

    .article {
      animation: rotateOn 1s ease-in-out;
      animation-fill-mode: forwards;
    }
  }

  &.off {
    animation: off 1s ease-in-out;
    animation-fill-mode: forwards;

    .article {
      animation: rotateOff 1s ease-in-out;
      animation-fill-mode: forwards;
    }
  }
}

#footer {
  position: absolute;
  bottom: 0; right: 0;
  width: 300px; height: 300px;
  background: $subColor;
  transform: rotate(-45deg) translateY(250px);
  z-index: 10;
  box-shadow: 0 0 5px rgba(0, 0, 0, .5);

  .txt {
    position: absolute;
    top: 15px; left: 0;
    width: 100%;
    font-size: 12px;
    text-align: center;
  }
}

.btn.close {
  position: absolute;
  top: -60px; right: 10px;
  width: 50px; height: 50px;
  transition: top .2s ease-in-out;
  z-index: 30;
  cursor: pointer;

  &:before {
    display: block;
    position: absolute;
    top: 0; bottom: 0;
    left: 0; right: 0;
    margin: auto;
    width: 2px; height: 30px;
    content: "";
    background: rgba(0, 0, 0, .5);
    transform: rotate(45deg);
  }

  &:after {
    display: block;
    position: absolute;
    top: 0; bottom: 0;
    left: 0; right: 0;
    margin: auto;
    width: 2px; height: 30px;
    content: "";
    background: rgba(0, 0, 0, .5);
    transform: rotate(-45deg);
  }

  &.on {
    top: 10px;
  }
}

@keyframes on {
  0% {
    z-index: 2;
  }

  50% {
    z-index: 20;
  }

  100% {
    z-index: 20;
  }
}

@keyframes off {
  0% {
    z-index: 20;
  }

  70% {
    z-index: 2;
  }

  100% {
    z-index: 2;
  }
}

@keyframes rotateOn {
  0% {
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    transform: scale(1) rotateZ(-5deg) rotateX(0deg) rotateY(0deg);
  }

  5% {
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    transform: scale(1) rotateZ(-5deg) rotateX(0deg) rotateY(0deg);
  }

  40% {
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    transform: scale(.98) rotateZ(-15deg) rotateX(-55deg) rotateY(-20deg)
  }

  95% {
    top: 0; bottom: 0;
    left: 0; right: 0;
    transform: scale(1) rotateZ(0deg) rotateX(0deg) rotateY(0deg);
  }

  100% {
    top: 0; bottom: 0;
    left: 0; right: 0;
    transform: scale(1) rotateZ(0deg) rotateX(0deg) rotateY(0deg);
  }
}

@keyframes rotateOff {
  0% {
    top: 0; bottom: 0;
    left: 0; right: 0;
    transform: scale(1) rotateZ(0deg) rotateX(0deg) rotateY(0deg);
  }

  5% {
    top: 0; bottom: 0;
    left: 0; right: 0;
    transform: scale(1) rotateZ(0deg) rotateX(0deg) rotateY(0deg);
  }

  60% {
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    transform: scale(.98) rotateZ(-15deg) rotateX(-55deg) rotateY(-20deg)
  }

  95% {
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    transform: scale(1) rotateZ(-5deg) rotateX(0deg) rotateY(0deg);
  }

  100% {
    top: 100px; bottom: 100px;
    left: 25px; right: 25px;
    transform: scale(1) rotateZ(-5deg) rotateX(0deg) rotateY(0deg);
  }
}
JavaScript
(function(win) {

  "use strict";

  win.App = win.App || {};

})(this);
(function(win, doc, ns) {

  "use strict";

  ns.Util = ns.Util || {};

  function _copyArray(array) {
    var newArray = [],
      i = 0;

    try {
      newArray = [].slice.call(array);
    } catch (e) {
      for (; i < array.length; i++) {
        newArray.push(array[i]);
      }
    }

    return newArray;
  }

  function EventDispatcher() {
    this._events = {};
  }

  EventDispatcher.prototype.hasEventListener = function(eventName) {
    return !!this._events[eventName];
  };

  EventDispatcher.prototype.addEventListener = function(eventName, callback) {
    if (this.hasEventListener(eventName)) {
      var events = this._events[eventName],
        length = events.length,
        i = 0;

      for (; i < length; i++) {
        if (events[i] === callback) {
          return;
        }
      }
      events.push(callback);
    } else {
      this._events[eventName] = [callback];
    }
  };

  EventDispatcher.prototype.removeEventListener = function(eventName, callback) {
    if (!this.hasEventListener(eventName)) {
      return;
    } else {
      var events = this._events[eventName],
        i = events.length,
        index;

      while (i--) {
        if (events[i] === callback) {
          index = i;
        }
      }

      events.splice(index, 1);
    }
  };

  EventDispatcher.prototype.fireEvent = function(eventName, opt_this, opt_arg) {
    if (!this.hasEventListener(eventName)) {
      return;
    } else {
      var events = this._events[eventName],
        copyEvents = _copyArray(events),
        arg = _copyArray(arguments),
        length = events.length,
        i = 0;

      // eventNameとopt_thisを削除
      arg.splice(0, 2);

      for (; i < length; i++) {
        copyEvents[i].apply(opt_this || this, arg);
      }
    }
  };

  ns.Util.EventDispatcher = EventDispatcher;

})(this, document, App);
(function(win, doc, ns) {

  "use strict";

  ns.Util = ns.Util || {};

  function Deferred() {
    var success = new SimpleDeferred(),
      miss = new SimpleDeferred();

    function done(callback) {
      success.done.call(this, callback);
    }

    function fail(callback) {
      miss.done.call(this, callback);
    }

    function resolve(opt_org) {
      success.resolve.call(this, opt_org);
      miss.reject();
    }

    function reject(opt_org) {
      success.reject();
      miss.resolve.call(this, opt_org);
    }

    function SimpleDeferred() {
      var queue = [],
        arg = null,
        isReject = false;

      function done(callback) {
        if (isReject) {
          return;
        }

        if (!!queue) {
          queue.push(callback);
        } else {
          callback.apply(this, arg);
        }
      }

      function resolve(opt_arg) {
        var length = queue.length,
          i;

        if (isReject) {
          return;
        }

        arg = !!opt_arg ? arguments : null;

        for (i = 0; i < length; i++) {
          queue[i].apply(this, arg);
        }

        queue = null;
      }

      function reject() {
        if (!!queue) {
          isReject = true;
        }
      }

      return {
        done: done,
        resolve: resolve,
        reject: reject
      };
    }

    return {
      done: done,
      fail: fail,
      resolve: resolve,
      reject: reject
    };
  }

  // export
  ns.Util.Deferred = Deferred;

})(this, document, App);
(function(win, doc, ns) {

  "use strict";

  ns.Util = ns.Util || {};

  function Throttle(opt_interval, opt_callback) {
    this._timer = null;
    this._lastEventTime = 0;
    this._interval = opt_interval || 500;
    this._callback = opt_callback || function() {};
  }

  Throttle.prototype.setInterval = function(ms) {
    this._interval = ms;
  };

  Throttle.prototype.addEvent = function(fn) {
    this._callback = fn;
  };

  Throttle.prototype.fireEvent = function(opt_arg) {
    var _this = this,
      currentTime = new Date() - 0,
      timerInterval = this._interval / 10;

    clearTimeout(_this.timer);

    if (currentTime - _this._lastEventTime > _this._interval) {
      _fire();
    } else {
      _this.timer = setTimeout(_fire, timerInterval);
    }

    function _fire() {
      _this._callback.call(_this, opt_arg || null);
      _this._lastEventTime = currentTime;
    }
  };

  ns.Util.Throttle = Throttle;

})(this, document, App);
(function(win, doc) {

  "use strict";

  var lists = doc.getElementById("lists"),
    btnClose = doc.querySelector(".close"),
    start = {
      x: 0,
      y: 0
    },
    DIRECTION = {
      NULL: "",
      NEXT: "next",
      PREV: "prev"
    },
    isOn = false,
    target,
    direction, prevback, prev, main, next, nextback;

  btnClose.addEventListener("click", function() {
    this.classList.remove("on");
    target.classList.add("off");

    setTimeout(function() {
      target.classList.remove("on");
      target.classList.remove("off");
    }, 1000);
  }, false);

  delegate(lists, "click", ".main .article", function() {
    target = this.parentNode;

    if (target.classList.contains("on")) {
      return;
    }

    target.classList.add("on");

    setTimeout(function() {
      btnClose.classList.add("on");
    }, 1000);
  });

  lists.addEventListener("mousedown", initGesture, false);
  lists.addEventListener("touchstart", initGesture, false);

  lists.addEventListener("mouseup", function(evt) {
    lists.removeEventListener("mousemove", handleTouchMove, false);
    direction = DIRECTION.NULL;
  }, false);

  lists.addEventListener("touchend", function(evt) {
    doc.removeEventListener("touchmove", preventDefault, true);
    lists.removeEventListener("touchmove", handleTouchMove, false);
    direction = DIRECTION.NULL;
  }, false);

  function initGesture(evt) {
    var isOn = false;

    (function checkOn(elm) {
      if (elm.classList.contains("on")) {
        isOn = true;
      } else if (elm.parentNode !== doc) {
        checkOn(elm.parentNode);
      }
    })(evt.target);

    if (isOn) {
      return;
    }

    if (evt.touches) {
      doc.addEventListener("touchmove", preventDefault, true);
      lists.addEventListener("touchmove", handleTouchMove, false);
    } else {
      lists.addEventListener("mousemove", handleTouchMove, false);
    }

    start.x = evt.touches ? evt.touches[0].pageX : evt.pageX;
    start.y = evt.touches ? evt.touches[0].pageY : evt.pageY;
    main = doc.querySelector(".main");
    prev = doc.querySelector(".prev");
    next = doc.querySelector(".next");
    prevback = doc.querySelector(".prevback");
    nextback = doc.querySelector(".nextback");
  }

  function preventDefault(evt) {
    evt.preventDefault();
  }

  function handleTouchMove(evt) {
    var pageY = evt.touches ? evt.touches[0].pageY : evt.pageY;

    if (pageY - start.y > 0) {
      lists.className = "down";
    } else {
      lists.className = "up";
    }

    if (pageY - start.y < -50 && direction !== DIRECTION.PREV) {
      direction = DIRECTION.PREV;

      main.classList.add("prev");
      main.classList.remove("main");

      next.classList.add("main");
      next.classList.remove("next");

      prev.classList.add("prevback");
      prev.classList.remove("prev");

      nextback.classList.add("next");
      nextback.classList.remove("nextback");

      setTimeout(function() {
        prevback.classList.add("nextback");
        prevback.classList.remove("prevback");
        initGesture(evt);
      }, 0);

    }

    if (pageY - start.y > 50 && direction !== DIRECTION.NEXT) {
      direction = DIRECTION.NEXT;

      main.classList.add("next");
      main.classList.remove("main");

      prev.classList.add("main");
      prev.classList.remove("prev");

      next.classList.add("nextback");
      next.classList.remove("next");

      prevback.classList.add("prev");
      prevback.classList.remove("prevback");

      setTimeout(function() {
        nextback.classList.add("prevback");
        nextback.classList.remove("nextback");
        initGesture(evt);
      }, 0);
    }
  }

  function delegate(parent, eventName, selector, callback) {
    parent.addEventListener(eventName, function(evt) {

      (function checkTarget(target) {
        if (target.matches(selector, parent)) {
          callback.call(target);
        } else {
          if (target === parent) {
            return;
          } else {
            checkTarget(target.parentNode);
          }
        }
      })(evt.target);

    }, false);
  }

})(this, document);