如何在 Javascript 中使用长按创建 iPhone 界面

抖动图标和长按已经成为我们对手机屏幕非常熟悉的东西。特别是在 iPhone 上,摇动图标通常意味着它们是可拖动和可编辑的——而长按已成为获得额外选项的正常方式。

在本教程中,我们将着眼于使用 Javascript 和 CSS 重新创建这些效果。在本教程中,我们将介绍:

  • 新的 CSS 功能– 例如背景模糊和动画弹性框。
  • 长按– 如何使用 Javascript 创建长按效果。
  • 拖动– 如何在 Javascript 中创建简单的拖放系统。

演示#

一如既往,让我们从演示开始。这就是我们今天计划创建的。

  • 如果您单击并按住图标一秒钟,将弹出一个弹出窗口。
  • 如果您总共点击并按下 2 秒钟,图标将开始晃动 – 就像在 iPhone 上一样。这也适用于移动设备。

第 1 步:HTML#

在本教程中,我不会过多地关注 HTML,因为它非常初级 – 足以说明此演示的 HTML 由以下部分组成:

  • 图标容器– 一个容器 div,其中包含有关各个图标的所有信息。
  • 封面– 位于整个演示顶部的封面 div,如果用户长按以显示子菜单,它将模糊。
  • 一些通知– 底部的一些通知提供一些操作说明。
  • 一个子菜单– 一个子菜单,当用户点击各种图标时,它会四处移动。

第 2 步:CSS#

大多数 CSS 都是非常基础的,但是我会指出一些我使用过的重要且有趣的东西,它们是 HTML UI 设计中非常典型的东西。所以我们的目标是让图标可以拖动,但是有很多方法可以影响它。首先,我们有一个弹出的封面来模糊背景。然后我们有那些讨厌的图像,大多数浏览器都允许用户拖动。

为了解决大部分这些问题,我们可以在 CSS 中使用一个名为pointer-events: none;. 这意味着用户无法与我们的 HTML 元素进行交互。所以在模糊的封面上,我们在它不模糊时删除指针事件,并在背景模糊时添加它们。这意味着用户通常会点击这个 HTML 元素,直到我们想要模糊背景,此时它才变为活动状态

#iphone .cover {    
    position: absolute;
    top: -1rem;
    backdrop-filter: blur(4px);
    left: -1rem;
    width: calc(100% + 2rem);
    opacity: 0;
    z-index: 99999;
    height: calc(100% + 2rem);
    transition: all 0.2s ease-out;
    background: rgba(0,0,0,0);
    pointer-events: none; /* No pointer events! */
}
[data-dropdown="true"] #iphone .cover {
    pointer-events: all; /* All pointer events */
    opacity: 1; 
}
#iphone .icon-container img {
    width: calc(100% + 2px);
    pointer-events: none; /* No pointer events */
}

背景滤镜

这将我们带到另一个非常有用的 CSS 属性 – backdrop-filter。除了 Firefox 之外的所有主要浏览器都支持背景过滤器,如果标签稍微透明,我们可以为 HTML 标签后面的元素添加效果。在我们的.coverCSS 中,我们将其定义为:

backdrop-filter: blur(4px);

动画图标删除

对于整个演示,我希望能够顺利移除图标。图标根据弹性框排列- 为此,我创建了一个自定义动画,使图标有效缩放为零(这样看起来它们会缩小) – 然后同时减小它们的宽度。效果是,当一个图标被移除时,图标似乎彼此靠近,从而创建流畅的动画。

当用户单击删除按钮时,此动画将应用在一小段 Javascript 中。

@keyframes scaleBack {
    0% {
        transform: scale(1);
        width: 5rem;
    }
    40% {
        opacity: 1;
    }
    50% {
        width: 5rem;
        opacity: 0;
        transform: scale(0.0001);
    }
    100% {
        width: 0rem;
        transform: scale(0.0001);
    }
}

使用转换的积极因素

  • 让我们对其他事物使用顶部、底部、左侧和右侧定位。
  • 使用 3d 加速,所以动画超级快!

第 2 步:Javascript#

这里的 Javascript 并不是非常复杂——事实上,很多复杂性来自于试图使图标可拖动。基本上我们要跟踪几个事件:

  • 当用户点击一个图标时——我们想要测量多长时间,这样我们就可以显示下拉菜单或摇动效果。
  • 当用户点击按钮时– 我们想要删除长按计时器,并重置任何其他运动跟踪器。
  • 当用户单击删除按钮时——我们要删除该图标。
  • 当用户点击按钮以外的任何东西时——我们想要移除效果。

因此,您可能已经猜到了,我们使用指针事件。我总是建议您尽可能尝试使用指针事件,因为它们既兼容移动端,也兼容桌面端。这样一来,您通常只需要为所有设备编写一个脚本。Pointer Events 将mousedown等事件处理程序替换为pointerdown

跟踪状态

为了跟踪用户,我们有两种状态 – shakingdropdown。我们使用 HTML 标签中的数据属性来跟踪这一点——如果图标在晃动,则data-shaking="true"在 body 标签上显示,如果下拉菜单可见,则data-dropdown="true"在 body 标签上显示。我们这样做是为了可以根据状态更改元素的 CSS。

然后我们有长按。要管理长按,我们要做的就是:

  • 创建一个 pointerdown 变量——这个变量在用户点击时立即设置为 true,在他们释放时立即设置为 false。
  • 创建超时– 我们在 1 秒和 2 秒后检查 pointerdown 是否为。如果是,那么我们要么使图标显示下拉菜单,要么摇动。
  • 设置重置功能– 如果用户放弃长按,我们会重置所有变量,因此如果他们再次尝试,我们可以再次使用它们。

在代码中,这看起来像这样:

// For selection of the icons and removal icons
let icons = document.querySelectorAll('.icon-container');
let removals = document.querySelectorAll('.remove-icon');

// These all store data on the mouse position at different times in the code
let pointerdown = false;
let offset = 0;
let mouseXInit = 0;
let mouseYInit = 0;
let mouseX = 0;
let mouseY = 0;
let positionX = 0;
let positionY = 0;
let currentTop = 0;
let currentLeft = 0;

// This is for holding our timers
let timers = { first: undefined, second: undefined, third: undefined }


let helpers = {
    reset: function(extended) {
        // This is our reset - it sets everything back to zero, whenever we need to
        // All variables and settings are reset
        mouseX = 0;
        mouseY = 0;
        mouseXInit = 0;
        mouseYInit = 0;
        currentTop = 0;
        currentLeft = 0;
        offset = 0;
        if(typeof timers.first !== "undefined") {
            clearTimeout(timers.first);
        }
        if(typeof timers.second !== "undefined") {
            clearTimeout(timers.second);
        }
        if(typeof timers.third !== "undefined") {
            clearTimeout(timers.third);
        }
        if(typeof extended == "undefined") {
            document.querySelector('.sub-menu').classList.remove('show-sub-menu');
            document.body.setAttribute('data-shaking', false);
            document.body.setAttribute('data-dropdown', false);
            pointerdown = false;
            icons.forEach(function(item) {
                item.setAttribute('data-selected', false);
                item.style.top = 0;
                item.style.left = 0;
            })
        }
    },
    checkPoint: function(x, y, limit) {
        // This checks if the users mouse has moved more than a certain limit. If it has, then they may be dragging..
        // So we don't cause the long press animation
        if(x < limit && x > limit * -1 && y < limit && y > limit * -1) {
            return true;
        } else {
            return false;
        }
    }
}

// For every icon 
icons.forEach(function(item) {
    // Add a pointerdown event
    item.addEventListener('pointerdown', function(e) {
        // Get the click location and set pointerdown to true
        pointerdown = true;
        mouseXInit = e.pageX;
        mouseYInit = e.pageY;
        
        // Get the left and top position of the item, if any
        currentTop = parseFloat(item.style.top) || 0;
        currentLeft = parseFloat(item.style.left) || 0;
        // Set a timer to wait for a hold click
        timers.first = setTimeout(function() {
            // Only do this if pointerdown is true, and if the user hasn't moved more than 10px while clicking down
            if(pointerdown === true && document.body.getAttribute('data-shaking') !== "true" && helpers.checkPoint(mouseX, mouseY, 10)) {
                
                // Icon is now selected, and the dropdown should appear
                item.setAttribute('data-selected', true);
                document.body.setAttribute('data-dropdown', true);
                
                // Find out where exactly the icon is (x, y) coordinates
                let left = item.getBoundingClientRect().left - document.querySelector('#iphone').getBoundingClientRect().left;
                let bottom = item.getBoundingClientRect().bottom - document.querySelector('#iphone').getBoundingClientRect().top;
                // Show the sub menu and move it to where the icon is
                document.querySelector('.sub-menu').classList.add('show-sub-menu');
                document.querySelector('.sub-menu').style.left = `${left}px`;
                document.querySelector('.sub-menu').style.top = `${bottom - 16}px`;

            }
        }, 1000);
        // If the user is still clicking after 2 seconds
        timers.second = setTimeout(function() {
            // Check they are clicking
            if(pointerdown === true && helpers.checkPoint(mouseX, mouseY, 10)) {
                // Now all icons should shake
                document.body.setAttribute('data-shaking', true);
                item.setAttribute('data-dragging', true);
                // Hide the sub menu
                document.querySelector('.sub-menu').classList.remove('show-sub-menu');
                document.body.setAttribute('data-dropdown', false);
                // Give each animation for shaking a delay, to give the appearance of randomness
                timers.third = setTimeout(function() {
                    icons.forEach(function(i) {
                        i.style.animationDelay = `${offset}s`;
                        offset += 0.1;
                    })
                }, 300);
            }
        }, 2000);
        // If the icons are shaking, then the user may be trying to drag this particular icon. Set that icon
        // to have a data-dragging of true. We can use this later
        if(document.body.getAttribute('data-shaking') === "true") {
            item.setAttribute('data-dragging', true);
        }
    });
    // if the user lifts their mouse, then reset everything
    item.addEventListener('pointerup', function() {
        helpers.reset(false);
    });
})

移动

当用户单击一个图标时,我们必须为该图标的移除设置动画。为此,我们首先添加一个动画,使图标缩小并缩小到0. 我们在之前的 CSS 部分谈到了这一点。然后在动画结束后,我们设置另一个超时以从 HTML 中完全删除图标。

removals.forEach(function(item) {
    item.addEventListener('click', function(e) {
        // If the removal icon is clicked, then get the parent HTML element - i.e. the icon itself
        let icon = item.parentNode;
        // Animate the icon to disappear
        icon.style.animation = 'scaleBack 0.4s linear 1 forwards';
        // Remove the dropdown, if it is around 
        document.body.setAttribute('data-dropdown', false);
        // And finally, delete the icon completely using the remove() function.
        setTimeout(() => {
            icon.remove();
        }, 400);
    })  
});

可拖动性#

接下来,让我们实现一个基本的拖动形式。拖拽在概念上可以分解成几块:

  • 首先,当用户点击页面时,我们使用e.clientX, e.clientY. 这为我们提供了他们点击的坐标。
  • 当他们移动并按下指针时,我们会发现他们的新位置与原始点击位置之间的差异。这个差值就是移动的总量。
  • 然后我们将该数量添加到该图标的topleftCSS 值。这给了我们阻力效果。最终,如果用户停止拖动,我们将重置所有值,以便图标快速回到原来的位置。

由于我们需要跟踪很多事情,因此我们有很多变量。我们可以使用 跟踪初始鼠标位置mouseXInit, mouseYInitpositionX, positionY然后,在激活摇动后,将差异存储在 中。mouseX, mouseY我们还在摇晃开始前单独存放。如果用户在点击时移动太多,我们不会激活长按效果,所以我们可以用 来检查mouseX, mouseY

在代码中,我们最终得到这样的结果:

document.body.addEventListener('mousemove', function(e) {
    // If the user is clicking down
    if(pointerdown === true) {
        // Track how much they're moving. If it's too much, we'll cancel the long press timeout
        mouseY = mouseXInit - e.pageY;
        mouseX = mouseXInit - e.pageX;
        if(document.body.getAttribute('data-shaking') == "true") {
            // If they are moving around after shaking starts, then they are dragging
            positionX = mouseXInit - e.pageX;
            positionY = mouseYInit - e.pageY;
            // Set the element to have a data-dragging attribute of true
            let el = document.querySelector('[data-dragging="true"]');
            if(el !== null) {
                // Move the element around
                el.style.top = `${positionY * -1 + currentTop}px`;
                el.style.left = `${positionX * -1 + currentLeft}px`;
            }
        }
    }
})

// When the user lifts their pointer up, then reset all the variables
document.body.addEventListener('pointerup', function(e) {
    if(!e.target.matches('.remove-icon')) {
        helpers.reset(false);
    }
    // And end all icon dragging by setting data-dragging to false on all icons.
    icons.forEach(function(item) {
        item.setAttribute('data-dragging', false);
    });
});

结论#

本教程到此结束。我希望你喜欢这篇文章,并且可能学到了一些新的 CSS 技能。一如既往,这里有一些有用的链接: