きっかけは この記事 を読んだことにあります。
(もう1年以上前の記事なんですね)
いろいろと参考になることが書いてあるのですが、なかでも一番気になったポイントは右手でスマートフォンを操作する人の指の届く範囲を表したこの図でした。
http://gigazine.net/news/20141209-ios-android-ux-design/
画面上に、あきらかに押しやすい箇所と押しにくい箇所があります。
であればいっそ、押しやすい箇所を操作範囲としたUIをつくればいいんじゃなかろうか。
そんなことを思ってつくったモックがこちらです。
http://codepen.io/kimmy/full/LGOaww/codepen.io
PCだとちょっと操作しにくいので、是非ともiPhone6以上のサイズのスマートフォンでご観覧頂きたいです。
(iPhoneでしか確認してないのでAndroidでは動かないかもしれません)
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">© 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);