制作动画滑块 - WotW
欢迎来到本周小部件系列,在这里我拍摄精彩的 UI/UX 组件的 gif 或视频,并通过代码使它们栩栩如生。
这次我们要创建一个温度滑块,虽然它用途广泛。
灵感来自ramykhuffash的这个作品,它看起来像这样:
准备工作
对于今天的小部件,我们将使用Vue.js,对于某些动画,我们将使用TweenMax 。此外,我们还需要一个温度图标,因此我们将使用Font Awesome中的图标。
如果您想继续,您可以分叉这个已经具有依赖项的codepen 模板。
匹配设计
这个小部件的 HTML 标记比平常稍微复杂一些,所以这次我将使用 HTML + CSS 将其分解成几个部分,直到我们匹配原始设计。
让我们首先设置上部和下部,上部包含数字,下部包含滑块控件。
<div id="app" class="main-container">
<div class="upper-container">
</div>
<div class="lower-container">
</div>
</div>
在设置样式之前,我们需要几个主要的 CSS 属性body
。
body {
margin: 0;
color: white;
font-family: Arial, Helvetica, sans-serif;
}
我们将边距设置为 ,0
以避免 周围出现间隙main-container
。color
和 也font-family
设置在那里,以避免在我们的元素上重复它们。
现在我们将使用CSS grid
属性将屏幕分成两部分,上面的部分需要采用类似于3/4
垂直高度的值,我们可以使用来实现fr
。
.main-container {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 3fr 1fr;
height: 100vh;
overflow-x: hidden;
}
注意属性100vh
中的值height
,即使我们的 div 根本没有内容,它也允许我们垂直填充屏幕。
此外,overflow-x: hidden
如果我们的元素超出较小屏幕的界限,该属性将阻止我们的小部件显示滚动条(感谢Nested Software注意到这一点)。
现在只需为各个部分添加背景色即可。对于上面的部分,我们将使用渐变:
.upper-container {
position: relative;
background: linear-gradient(to bottom right, #5564C2, #3A2E8D);
}
.lower-container {
background-color: #12132C;
}
当我们用托盘定位其内部元素时,position: relative
设置的属性将会有用。upper-container
上部内的数字似乎是合乎逻辑的下一步。
<!-- inside .upper-container -->
<h2 class="temperature-text">10</h2>
这将是显示当前温度的大数字,让我们使用一些 CSS 来更好地定位它:
.temperature-text {
position: absolute;
bottom: 150px;
font-size: 100px;
width: 100%;
text-align: center;
user-select: none;
}
该user-select: none
属性应该可以帮助我们避免在与滑块交互时选择文本。
在我们添加下面出现的数字之前,让我们用一些数据启动 Vue 实例,以帮助我们避免重复不必要的标记元素:
new Vue({
el: '#app',
data: {
temperatureGrades: [10, 15, 20, 25, 30]
}
})
现在我们可以使用该temperatureGrades
数组来显示设计中的这些元素:
<!-- just after .temperature-text -->
<div class="temperature-graduation">
<div class="temperature-element"
v-for="el in temperatureGrades"
:key="el">
<span class="temperature-element-number">{{el}}</span><br>
<span class="temperature-element-line">|</span>
</div>
</div>
请注意,我们为每个数字渲染一个|
字符,现在我们可以将它们设计成看起来像“尺子”。
对于数字和行,我们需要将文本居中,我们会在temperature-element
规则内进行操作。我们还会将元素设置inline-blocks
为彼此相邻。最后,|
字符需要更小,font-size
我们会处理好这一点:
.temperature-element {
text-align: center;
display: inline-block;
width: 40px;
margin: 0 10px 0 10px;
opacity: 0.7;
}
.temperature-element-line {
font-size: 7px;
}
检查.temperature-graduation
元素我们可以看到它的宽度是 300px,为了使其居中我们可以使用以下方式计算的值:
.temperature-graduation {
position: absolute;
left: calc(50% - 150px); // subtracting half the width to center
bottom: 25px;
user-select: none;
}
我们还设置了bottom
属性,使其出现在下部正上方。
滑块
上面的部分已经完成了,现在我们来添加滑块控件。按钮很简单,只需要一个带有图标的 div 即可:
<!-- inside .lower-container -->
<div class="slider-container">
<div class="slider-button">
<i class="fas fa-thermometer-empty slider-icon"></i>
</div>
</div>
现在让我们来设置按钮的样式,以下大多数 CSS 代码都是手动“调整”的值,以便能够将元素定位在所需的位置。
.slider-container {
width: 150px;
height: 80px;
margin-top: -30px;
margin-left: calc(50% - 187px);
position: relative;
}
.slider-button {
position: absolute;
left: 42px;
top: 5px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #2724A2;
cursor: grab;
cursor: -webkit-grab;
cursor: -moz-grab;
}
.slider-icon {
margin-top: 16px;
margin-left: 21px;
color: white;
}
当鼠标悬停在按钮上时,按钮内的值grab
将把光标变成一只手。
现在的滑块只差一个“波浪”形状了。一开始我尝试用border-radius
数值和旋转来实现div
,但可惜的是,它与设计不符。最终我做的图形是SVG
这样的:
该形状的代码如下:
<!-- inside .slider-container -->
<svg width="150" height="30" viewBox="0 0 150 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M74.3132 0C47.0043 2.44032e-05 50.175 30 7.9179 30H144.27C99.4571 30 101.622 -2.44032e-05 74.3132 0Z" transform="translate(-7.38794 0.5)" fill="#12132C"/>
</svg>
互动
到目前为止,此小部件交互中最引人注目的是滑块的拖放操作。我们之前在制作卡片滑块时已经这样做过,所以我将采用类似的方法:
// inside data
dragging: false,
initialMouseX: 0,
sliderX: 0,
initialSliderX: 0
这些数据属性将帮助我们跟踪用户何时开始/停止拖动、鼠标和滑块的位置。
以下方法将在用户交互时初始化这些变量:
// after data
methods: {
startDrag (e) {
this.dragging = true
this.initialMouseX = e.pageX
this.initialSliderX = this.sliderX
},
stopDrag () {
this.dragging = false
},
mouseMoving (e) {
if(this.dragging) {
// TODO move the slider
}
}
}
现在让我们将它们绑定到模板
<div id="app" class="main-container"
@mousemove="mouseMoving"
@mouseUp="stopDrag">
<!-- ... inside .slider-container
<div class="slider-button"
@mouseDown="startDrag">
您可能已经注意到,操作@mouseDown
是在滑块按钮中设置的,但@mouseMove
和@mouseUp
处于主 div 的级别。
背后的原因是,用户首先会按下滑块按钮,但在移动光标时,他们通常会超出滑块轨道,如果他们在按钮外放开鼠标,它将不会被跟踪,并会导致按钮跟随您,直到您再次单击它。
现在让我们mouseMoving
用一个算法来填充该方法,该算法将sliderX
属性设置为所需的位置。我们需要为滑块声明一些约束,以匹配我们之前设置的标尺。
// before the Vue instance
const sliderMinX = 0
const sliderMaxX = 240
// inside mouseMoving method
// replace the "TODO" line with this:
const dragAmount = e.pageX - this.initialMouseX
const targetX = this.initialSliderX + dragAmount
// keep slider inside limits
this.sliderX = Math.max(Math.min(targetX, sliderMaxX), sliderMinX)
// after methods
computed: {
sliderStyle () {
return `transform: translate3d(${this.sliderX}px,0,0)`
}
}
您可能已经猜到了,计算属性sliderStyle
存储了滑块的位置,我们只需要将其绑定到.slider-container
:
<div class="slider-container" :style="sliderStyle">
我们几乎已经有一个可以工作的滑块控件了,但还缺少一个重要的东西:跟踪滑块的值。这听起来可能很复杂,但我们可以使用计算属性来计算该值,因为我们已经知道了sliderX
位置:
// inside computed
currentTemperature () {
const tempRangeStart = 10
const tempRange = 20 // from 10 - 30
return (this.sliderX / sliderMaxX * tempRange ) + tempRangeStart
}
您可以通过在元素内部渲染它来查看它是否有效.temperature-text
:
<h2 class="temperature-text">{{currentTemperature}}</h2>
现在的问题是它渲染的是浮点数。我们可以使用过滤器来避免这个问题:
// after data
filters: {
round (num) {
return Math.round(num)
}
},
现在我们可以像这样使用过滤器:
<h2 class="temperature-text">{{currentTemperature | round}}</h2>
最后的润色
我们可以就此打住,让小部件像这样,但仍然缺少一些细节。
当温度超过 25 度时,背景应该会改变颜色,并且标尺上的数字应该以波浪形移动。
对于背景,我们将在顶部声明几个常量和一些新的数据属性:
const coldGradient = {start: '#5564C2', end: '#3A2E8D'}
const hotGradient = {start:'#F0AE4B', end: '#9B4D1B'}
// inside Vue
// inside data
gradientStart: coldGradient.start,
gradientEnd: coldGradient.end
//inside computed
bgStyle () {
return `background: linear-gradient(to bottom right, ${this.gradientStart}, ${this.gradientEnd});`
}
它们将保存渐变背景所需的颜色。bgStyle
计算属性每次都会生成背景gradientStart
并gradientEnd
进行更改。让我们将它绑定到相应的 HTML 元素上:
<div class="upper-container" :style="bgStyle">
现在它看起来应该是一样的,但是当我们在方法中添加动画规则时,它就会改变mouseMoving
:
// set bg color
let targetGradient = coldGradient
if (this.currentTemperature >= 25) {
targetGradient = hotGradient
}
if(this.gradientStart !== targetGradient.start) {
// gradient changed
TweenLite.to(this, 0.7, {
'gradientStart': targetGradient.start,
'gradientEnd': targetGradient.end
})
}
我们要做的是,当温度变化到 25 度或以上时,将渐变值从冷色变为暖色。这个过渡效果是用 TweenLite 实现的,而不是 CSS 过渡,因为后者只适用于纯色。
Y
最后,如果滑块靠近标尺元素,则标尺元素需要改变其位置。
<div class="temperature-element" v-for="el in temperatureGrades"
:style="tempElementStyle(el)"
:key="el">
与上半部分类似,我们将通过一个方法绑定要更改的样式,该方法将接收每个标尺的值。现在只需进行一些数学运算来计算距离并生成一些 CSS 变换 props:
// inside methods
tempElementStyle (tempNumber) {
const nearDistance = 3
const liftDistance = 12
// lifts up the element when the current temperature is near it
const diff = Math.abs(this.currentTemperature - tempNumber)
const distY = (diff/nearDistance) - 1
// constrain the distance so that the element doesn't go to the bottom
const elementY = Math.min(distY*liftDistance, 0)
return `transform: translate3d(0, ${elementY}px, 0)`
}
现在来看看最终结果!
这就是本周的小部件。
如果您还想了解更多,可以查看其他 WotW:
此外,如果您想查看下周的特定小部件,请将其发布在评论部分。
鏂囩珷鏉ユ簮锛�https://dev.to/ederchrono/making-an-animated-slider---wotw-mkj