为响应式HTML5触摸界面构建灯箱

  • 所需知识:中级CSS,中级到高级JavaScript
  • 要求:Android或iOS触控设备
  • 项目时间:2-3个小时
  • 支持文件

自原版以来,Lightbox小部件一直是网络的标准配置Lightbox.js2005年发布。灯箱创建了一个模式对话框,用于查看大图像,通常使用“下一个”和“上一个”按钮在幻灯片之间导航。

由于触摸设备网站的使用爆炸已经更新了他们的灯箱以支持手势交互,并取得了不同程度的成功。在本教程中,我将向您展示如何使用手势支持创建一个简单的灯箱。在这个过程中,您将学习一些关于提高触摸界面感知性能的知识,以及一些提高实际性能的简单技巧。

为触摸设备编写代码与为桌面编写代码根本不同。您可以(并且应该)使用桌面代码共享尽可能多的代码,但是您所做的事情总会有很大差异。

基准测试显示,最常见的触摸设备在性能上与1998年左右的台式计算机相当。它们通常具有大约256MB的RAM和CPU性能,与原始的iMac相当。我们习惯在桌面上“正常工作”的技术不会在手机和平板电脑上工作得很好。幸运的是,这些设备通常针对图形进行了优化,特别是在屏幕上移动元素。 iOS设备和Android 3.0及更高版本具有硬件图形加速功能。

实际上,您可以将这些设备想象成蹩脚的电脑与体面的视频卡

在过去的20年里,我们一直以大致相同的方式与我们的台式机进行交互。我们移动鼠标指针并单击控件。按钮,关闭框,链接和滚动条是用户和开发人员的第二天性。触摸界面引入了完全不同的约定。最常见的新公约之一是刷卡。通过“滑动”,可以呈现多个项目,就好像它们在一行中一样,并且用户可以使用“滑动”手势在它们之间导航。

滑动是一种常见模式,您甚至不需要告诉用户它 - 当呈现看起来像列表时,用户会本能地尝试滑动。

我们经常无法让代码更快地运行,特别是当我们处理慢速连接和慢速设备时。但我们可以制作界面似乎通过专注于正确的优化来加快速度。

我最喜欢的优化感知性能的例子是TiVo。十三年前,当第一批TiVo盒出现时,它们的速度非常慢。 (16MB内存和54mhz CPU!)用遥控器点击某些东西后,可能需要花费很长时间才能完成某些事情,特别是如果你开始播放或录制。尽管如此,没有人抱怨过TiVo很慢。在我看来,这是因为声音。 TiVo界面中最熟悉的部分是单击按钮后播放的“哔哔声”声音。那声音立即播放。 TiVo的工程师确保尽可能快地加载声音,这样无论接下来发生什么,用户都知道界面没有死亡。那声音很小,告诉用户他们的请求已被听到。

在网络上,我们开发了一个做同样事情的约定:微调。点击后我们立即抛出一个微调器图形,这样用户就会收到他们已经听过的消息。在移动设备上,我们必须做出不同的事情

手势不是像点击这样的离散行为。然而,为了使界面看起来很快,我们必须给用户一些反馈。当他们做手势时,我们以某种方式移动界面,以便他们知道我们正在“听到”它们。

01.工具

感觉响应的接口要求元素尽可能快地移动 - 移动是我们向用户显示接口响应其请求的方式。为此使用JavaScript动画太慢了。相反,我们使用CSS变换和转换:转换性能和转换,以便动画可以在不阻止JavaScript执行的情况下运行。

在本教程中,我将对所有动作和动画使用变换和过渡。

我喜欢尽可能使用的另一个优化是我称之为“只写DOM”。从DOM中读取属性和值是昂贵的并且通常是不必要的。对于灯箱,我尝试将所有读数组合到初始化阶段。之后,我在JavaScript中维护状态,并在必要时进行简单的算术运算。

02.建造灯箱

在本教程中,我们将构建一个包含一些缩略图的页面。在缩略图上单击(或点击)将启动灯箱。一旦进入灯箱,用户就可以在图像之间滑动,然后点击“关闭”按钮离开灯箱。

构建手势界面时,请记住感知性能的重要性。在灯箱中,这意味着确保在用户滑动时幻灯片移动。当用户停止手势时,幻灯片应该动画到下一个位置,或者如果幻灯片没有前进则“快照”。

“回弹”动画至关重要。这是如何确保用户永远不会觉得界面死亡。

03.入门

创建以下文件:

灯箱/
Reset.css
Slides.css
Slides.html
Slides.js

The completed thumbnail page

完成的缩略图页面

04.锅炉

HTML将变得简单。这不仅仅是为了演示。根据定义,复杂的DOM树速度较慢。使用更复杂的DOM树,样式,dom元素检索和重绘都更加昂贵。因为我们的目标是蹩脚的计算机,所以每一点都很重要,所以从一开始就保持简单。

我正在使用Eric Meyer的reset.css开始使用干净的CSS平板。我也在设置视口,以便页面不可缩放。

我已禁用原生捏缩放,因为它会干扰手势。 (支持捏手势的正确方法是在JavaScript中重新实现它。捏缩放需要自己的教程,所以我们暂时忽略它。)

在JS方面,我正在使用Zepto.js,一个非常轻量级的框架,带有jQuery语法。没有框架是真正必要的,但它可以加快一些平凡任务的工作。对于实际的手势交互,我们将使用本机API。






触摸界面演示






05. HTML

我创建了一个div,其中包含一个无序的缩略图列表。唯一特别的是数据属性。对于我包含的每张图片数据全宽数据全高缩略图表示的全尺寸图像的高度和宽度。我可以在获取图像后从图像中获取此数字,但是预先设置它可以加载预览图像并构建节点,而不会让用户等待服务器响应。
































  • 06.设置缩略图的样式

    现在添加一些漂亮的缩略图和其他一些视觉效果:

    Html {
    背景:#f1eee4;
    Font-family:georgia;
    颜色:#7d7f94;
    }

    H1 {
    颜色:#ba4a00;
    }

    .welcome {
    Text-align:center;
    text-shadow:1px 1px 1px #fff;
    }

    .welcome H1 {
    Font-size:20px;
    Font-weight:bold;
    }

    .welcome {
    -webkit-box-sizing:border-box; / * Safari / Chrome,其他WebKit * /
    -moz-box-sizing:border-box; / * Firefox,其他Gecko * /
    box-sizing:border-box; / * Opera / IE 8+ * /
    保证金:5px的;
    填充:10px的;
    box-shadow:2px 2px 5px rgba(0,0,0,0.5);
    Border-radius:5px;
    }

    .carousel {
    保证金:5px的;
    }

    .carousel Ul Li {
    身高:70px;
    宽度:70px;
    保证金:5px;
    溢出:隐藏;
    显示:块;
    向左飘浮;
    Border-radius:5px;
    box-shadow:1px 1px 2px rgba(0,0,0,0.5), - 1px -1px 2px rgba(255,255,255,1);
    }

    基本的灯箱

    灯箱的JavaScript需要做一些不同的事情:

    07.实用功能

    而不是打字-webkit-变换Translate3d我一遍又一遍地创建了一些实用功能来为我做这项工作。

    Function Prefixify(str){

    var ua = window.navigator.userAgent;

    if(ua.indexOf('WebKit')!== -1){
    Return'-webkit-'+ Str;
    }

    if(ua.indexOf('Opera')!== -1){
    返回'-o-'+ Str;
    }

    if(ua.indexOf('Gecko')!== -1){
    返回'-moz-'+ Str;
    }

    返回str;
    }

    function setPosition(node,left){
    // node.css('left',left +'px');
    node.css(prefixify('transform'),“translate3d(”+ left +“px,0,0)”);
    }

    Function AddTransitions(node){
    node.css(prefixify('transition'),prefixify('transform')+'。25s ease-in-out');

    node [0] .addEventListener('webkitTransitionEnd',function(e){
    Window.setTimeout(函数(){
    $(e.target).css(' - webkit-transition','none');
    },0)
    })
    }

    Function CleanTransitions(node){
    node.css(prefixify('transition'),'none');

    }

    我们的灯箱小部件将在页面加载时初始化,以加快速度。初始化是在页面上找到所有缩略图以构建数据模型的问题。我们将等待灯箱何时显示为灯箱构建HTML并附加事件处理程序。

    08.初始化

    对于lightbox对象,我使用一个构造函数,它将容器节点选择器作为其唯一参数。

    //清理命名空间
    Window.saw =(function($){

    //灯箱构造函数
    Function Lightbox(selector){

    var container_node = $(selector),
    包装,
    ChromeBuilt,
    CurrentSlide = 0,
    SlideData = [],
    BoundingBox = [0,0],
    SlideMap = {};

    Function Init(){
    // Init函数
    }

    返回{

    Show:show,
    隐藏:隐藏
    };

    }

    返回{

    灯箱:灯箱

    };

    }($));

    init函数获取所有li节点,查找缩略图,并将信息记录在slideData数组中。同时我保留一个名为slideMap的对象,它将缩略图链接的href映射到数组中的slideData元素。这使我可以快速查找点击信息中的数据,而无需遍历数组中的所有数据或使用其他信息装饰DOM。

    Function Init(){
    var slides = container_node.find('li');
    slides.each(function(i,el){
    var thisSlide = {},thisImg = $(el).find('img');

    thisSlide.url = thisImg.attr('src');
    thisSlide.height = thisImg.attr('data-full-height');
    thisSlide.width = thisImg.attr('data-full-width');
    thisSlide.link = $(el).find('a')。attr('href');

    //将幻灯片信息推送到slideData数组,同时在slideMap对象中记录数组索引。
    slideMap [thisSlide.link] = slideData.push(thisSlide) - 1;
    });
    }

    其余的初始化发生在节目方法。

    //这是从内联脚本调用的函数
    Function Show(startSlide){

    如果(!chromeBuilt){
    BuildChrome();
    AttachEvents();
    }
    Wrapper.show();

    //跟踪视口大小
    boundingBox = [window.innerWidth,window.innerHeight];

    GOTO(slideMap [startSlide]);
    }

    09.建造铬合金

    BuildChromefunction为灯箱创建HTML包装器,然后设置信号量,以便每次用户隐藏或显示灯箱时都不会重新构建chrome。为简单起见,我为HTML本身创建了一个单独的模板函数:

    Var WrapperTemplate = Function(){
    回来'

    “+
    “+
    “;
    }


    Function BuildChrome(){
    wrapper = $(wrapperTemplate())。addClass('slidewrap');
    $( '身体')追加(包装);
    ChromeBuilt = True;
    }

    构建chrome的最后一步是为'next'和'previous'链接附加一个事件处理程序:

    Function HandleClicks(e){
    E.preventDefault();

    var targ = $(e.target);
    if(targ.hasClass('next')){
    GoTo(currentSlide + 1);
    } else if(targ.hasClass('prev')){
    GoTo(currentSlide - 1);
    } Else {
    隐藏();
    }

    }

    Function AttachEvents(){
    wrapper.on('click',handleClicks,false);
    }

    现在灯箱镀铬已准备好用于一些实际的幻灯片。在我的show函数中,我调用goTo()来加载起始幻灯片。此功能显示参数标识的幻灯片,但它也会根据需要懒惰地构建幻灯片。 (旁白:不要在没有camelcase的情况下调用函数goto,goto是JavaScript中的保留字)。

    10.构建幻灯片

    现在,我正在查看的幻灯片位于视口中,上一张和下一张幻灯片位于屏幕左侧和右侧。当用户点击(或滑动)“下一个”时,当前幻灯片向左移动,下一张幻灯片移动到位。

    //对于幻灯片,采用“幻灯片”对象
    Function SlideTemplate(slide){
    回来'

    '+ slide.id +'
    “;
    }

    我正在使用

    而不是一个因为(至少现在)移动浏览器在绘制时要慢得多比一个
    与背景图像。在处理移动设备时,通常优选快速校正。 ARIA角色可以轻松解决可访问性问题。

    BuildSlide函数本身更复杂。除了通过幻灯片模板推送幻灯片数据外,代码还必须确保幻灯片适合视口。这是一个简单的问题,即如果图像不适合,可以计算出图像的缩放程度。我们可以让浏览器处理调整大小。

    Function BuildSlide(slideNum){

    var thisSlide,s,img,scaleFactor = 1,w,h;

    if(!slideData [slideNum] || slideData [slideNum] .node){
    返回false;
    }

    var thisSlide = slideData [slideNum];
    var s = $(slideTemplate(thisSlide));

    var img = s.children('div');

    //图片太大了!缩放它!
    if(thisSlide.width> boundingBox [0] || thisSlide.height> boundingBox [1]){

    if(thisSlide.width> thisSlide.height){
    scaleFactor = boundingBox [0] /thisSlide.width;
    } Else {
    scaleFactor = boundingBox [1] /thisSlide.height;
    }

    w = Math.round(thisSlide.width * scaleFactor);
    h = Math.round(thisSlide.height * scaleFactor);
    img.css('height',h +'px');
    img.css('width',w +'px');

    }其他{
    img.css('height',thisSlide.height +'px');
    img.css('width',thisSlide.width +'px');
    }



    ThisSlide.node = S;
    Wrapper.append(一个或多个);

    //将新幻灯片放入开始的poisition中
    SetPosition(s,boundingBox [0]);

    回归;
    }

    The first slide displayed in the lightbox

    第一张幻灯片显示在灯箱中

    去吧

    GoTo将请求的幻灯片和相邻幻灯片移动到位。

    Function GoTo(slideNum){
    Var ThisSlide;

    //如果我们要查找的幻灯片不存在,那就让我们走吧
    //回到当前幻灯片这具有提供的便利效果
    //在拍摄时“快速反馈”反馈,幻灯片只会生成动画
    //回到原位
    如果(!slideData [slideNum]){
    返回;
    }

    ThisSlide = SlideData [slideNum];

    //构建相邻的幻灯片
    BuildSlide(slideNum);
    BuildSlide(slideNum + 1);
    BuildSlide(slideNum - 1);

    //让它看起来很棒
    AddTransitions(thisSlide.node);

    //将当前幻灯片置于适当位置
    SetPosition(thisSlide.node,0);

    //滑动相邻的幻灯片
    if(slideData [slideNum - 1] && slideData [slideNum-1] .node){
    addTransitions(slideData [slideNum - 1] .node);
    setPosition(slideData [slideNum - 1] .node,(0 - boundingBox [0]));
    }

    if(slideData [slideNum + 1] && slideData [slideNum + 1] .node){
    addTransitions(slideData [slideNum + 1] .node);
    setPosition(slideData [slideNum + 1] .node,boundingBox [0]);
    }

    //更新状态
    CurrentSlide = SlideNum;
    }

    此时灯箱的功能或多或少。我们可以转到下一张和上一张幻灯片,我们可以隐藏和显示。当它们到达第一张和最后一张幻灯片时,可能是理想的知道,可能是通过灰显控件。这是一个可用的灯箱,在桌面或触摸设备上。

    12.添加手势支持

    大多数触摸设备包括原生照片查看器。这些不同的应用程序,遵循原始的iPhone照片应用程序,创建了一个UI约定:向左滑动推进幻灯片。我已经看到这种互动的几种实现根本没有提供任何反馈;当手势完成时,幻灯片会简单地前进。最好的方法是提供实时反馈。当用户滑动时,滑动在用户手指下移动并且下一个(或前一个)滑动向左或向右显示。这给人一种幻觉,即用户正在拉动一条照片。

    Swiping between slides

    在幻灯片之间滑动

    13.听取触摸事件

    许多图书馆,包括Zepto,都包括对触摸事件的支持。一般来说,我不建议使用它们。处理触摸事件时,您将以用户手势更新元素。对于用户来说,这个位置对于延迟根本就是显而易见的:它会感觉慢。添加间接层必然会影响性能。间接永远不会自由。我们使用库进行事件的主要原因之一是提供浏览器规范化层。支持触摸事件的所有移动浏览器都具有相同的API。

    此示例需要考虑三个触摸事件:TouchstartTouchmoveTouchend。还有一个Touchcancel事件,当手势因某种原因而中断时(例如推送通知)。在制作中你应该优雅地处理这个问题。

    Function AttachTouchEvents(){

    var bd = document.querySelector('html');
    bd.addEventListener('touchmove',handleTouchEvents);
    bd.addEventListener('touchstart',handleTouchEvents);
    bd.addEventListener('touchend',handleTouchEvents);

    }

    事件处理程序接收TouchEvent对象。该TouchstartTouchmove事件包含一个触摸属性是一个数组触摸对象。滑动只需要一个属性:ClientX。此值是触摸相对于页面左上角的位置。

    IOS设备最多支持11种触摸。 Android(冰淇淋三明治之前)只包含一个。大多数互动只需要一次触摸。更复杂的手势意味着担心多次接触。

    14. HandleTouchEvents函数

    首先在此函数外定义一些变量来维护状态:

    var startPos,endPos,lastPos;

    下一个分支基于事件对象的type属性:

    Function HandleTouchEvents(e){

    Var Direction = 0;

    //你也可以使用switch语句
    if(e.type =='touchstart'){

    } else if(e.type =='touchmove'){

    } else if(e.type =='touchend){

    }

    Touchstart事件在任何触摸事件开始时触发,因此使用它来记录手势开始的位置,这在以后会很重要。清除可能仍在节点上的任何转换。

    if(e.type =='touchstart'){

    //记录启动clientX
    startPos = e.touches [0] .clientX;

    // LastPos在开头是startPos
    LastPos = StartPos;

    //我们将跟踪方向作为有符号整数。
    // -1是左边,1是右边,0是静止不动
    Direction = 0;

    //现在我们清理转换
    if(slideData [currentSlide] && slideData [currentSlide] .node){
    cleanTransitions(slideData [currentSlide] .node);
    }

    if(slideData [currentSlide + 1] && slideData [currentSlide + 1] .node){
    cleanTransitions(slideData [currentSlide + 1] .node);
    }

    if(slideData [currentSlide - 1] && slideData [currentSlide -1] .node){
    cleanTransitions(slideData [currentSlide -1] .node);
    }

    } else if(e.type =='touchmove'){

    在里面Touchmove找出触摸移动了多少ClientX,然后移动当前幻灯片相同的金额。如果幻灯片向左移动,您还可以移动下一张幻灯片,如果它向右移动,则移动上一张幻灯片。这样你就只能移动两个节点,但是你会给出整个条带移动的错觉。拥有幻灯片信息的映射允许您在不进行任何DOM读取的情况下完成所有这些操作,只需写入。

    } else if(e.type =='touchmove'){
    E.preventDefault();

    //弄清楚方向
    if(lastPos> startPos){
    Direction = -1;
    }其他{
    Direction = 1;
    }

    //确保幻灯片存在
    如果(slideData [currentSlide]){

    //将当前幻灯片移动到位
    setPosition(slideData [currentSlide] .node,e.touches [0] .clientX - startPos);

    //确保下一张或上一张幻灯片退出
    if(direction!== 0 && slideData [currentSlide + direction]){

    //移动下一张或上一张幻灯片。
    If(direction <0){

    //我想将下一张幻灯片移动到正确的位置,这与...相同
    //当前幻灯片,减去视口的宽度(每个幻灯片与视口一样宽)
    setPosition(slideData [currentSlide + direction] .node,(e.touches [0] .clientX - startPos) - boundingBox [0]);
    } else if(direction> 0){

    setPosition(slideData [currentSlide + direction] .node,(e.touches [0] .clientX - startPos)+ boundingBox [0]);
    }

    }
    }

    //保存最后一个位置,我们需要它用于触摸结束
    lastPos = e.touches [0] .clientX;
    } else if(e.type =='touchend'){

    当触摸结束决定是推进,返回还是什么也不做。如果没有任何幻灯片需要“快速恢复”到位,请向用户反馈幻灯片没有改变的原因。

    } else if(e.type =='touchend'){

    //弄清楚我们是否向左或向右移动超过阈值
    //(在这种情况下为50像素)

    if(lastPos - startPos> 50){
    GOTO(currentSlide-1);
    } else if(lastPos - startPos <-50){
    GOTO(currentSlide + 1);
    }其他{

    //我们没有前进,所以我们需要“回击”到之前的位置
    addTransitions(slideData [currentSlide] .node);
    setPosition(slideData [currentSlide] .node,0);

    if(slideData [currentSlide + 1] && slideData [currentSlide + 1] .node){
    AddTransitions(slideData [currentSlide + 1]);
    setPosition(slideData [currentSlide + 1] .node,boundingBox [0]);
    }

    if(slideData [currentSlide - 1] && slideData [currentSlide - 1] .node){
    addTransitions(slideData [currentSlide - 1]);
    setPosition(slideData [currentSlide - 1] .node,0 - boundingBox [0]);
    }

    }
    }

    Hitting the end of the slides

    点击幻灯片的结尾

    现在基础已经到位。你有一个简单的触摸灯箱!

    斯蒂芬伍兹在十年的大部分时间里,我一直在构建Web应用程序。他目前住在旧金山,在Flickr担任前端工程师。

    查看更多net Magazine Javascript

    话题

    相关文章



    翻译字数超限