构建自适应SVG:使用symbol、use和CSS媒体查询
SVG图形确实可以缩放,但如何让它们更好地适应不同的屏幕尺寸?网页设计先驱Andy Clarke解释了如何使用<symbol>、<use>和CSS媒体查询构建他称之为“自适应SVG”的技术。
问题背景
假设我设计了一个基于《Bow Wow Bandit》的SVG场景,采用16:9宽高比,viewBox尺寸为1920×1080。这种SVG可以放大和缩小,但在小屏幕上,16:9的宽高比可能不是最佳格式,图像会失去其冲击力。有时,像3:4这样的竖屏方向更适合小屏幕尺寸。
但问题在于,仅使用viewBox很难为不同的屏幕尺寸重新定位内部元素。这是因为在SVG中,内部元素的位置锁定在原始viewBox的坐标系上,因此无法轻松在桌面和移动设备之间更改它们的布局。
解决方案探索
显示和隐藏SVG
最明显的选择是在标记中包含两个不同的SVG,一个用于小屏幕,另一个用于大屏幕,然后使用CSS和媒体查询显示或隐藏它们:
1
2
3
4
5
6
7
|
<svg id="svg-small" viewBox="0 0 1080 1440">
<!-- ... -->
</svg>
<svg id="svg-large" viewBox="0 0 1920 1080">
<!--... -->
</svg>
|
1
2
3
4
5
6
7
|
#svg-small { display: block; }
#svg-large { display: none; }
@media (min-width: 64rem) {
#svg-small { display: none; }
#svg-mobile { display: block; }
}
|
但使用这种方法,两个SVG版本都会被加载,当图形复杂时,这意味着下载大量不必要的代码。
使用JavaScript替换SVG
考虑使用JavaScript在指定断点处交换更大的SVG:
1
2
3
4
5
|
if (window.matchMedia('(min-width: 64rem)').matches) {
svgContainer.innerHTML = desktopSVG;
} else {
svgContainer.innerHTML = mobileSVG;
}
|
除了JavaScript现在对设计显示至关重要之外,两个SVG通常都会被加载,这会增加DOM复杂性和不必要的重量。此外,维护也成了问题,因为现在有两个版本的图形需要维护。
最终解决方案:一个SVG符号库和多个使用实例
我的目标是:
- 向小屏幕提供一个版本的Bow Wow Bandit
- 向大屏幕提供另一个版本
- 只定义一次图形(DRY原则)
- 能够调整元素大小和重新定位
使用symbol元素
<symbol>元素允许您定义可重用的SVG元素,这些元素可以隐藏和重用,以提高可维护性并减少代码膨胀。它们就像SVG的组件:创建一次,在需要的地方使用:
1
2
3
4
5
6
7
8
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="quick-draw-body" viewBox="0 0 620 700">
<g class="quick-draw-body">[…]</g>
</symbol>
<!-- ... -->
</svg>
<use href="#quick-draw-body" />
|
导出独立viewBox
当我创建符号时,需要它有自己的特定viewBox。我将每个元素导出为单独大小的SVG,这给了我将其内容转换为符号所需的尺寸。
以Quick Draw McGraw的帽子SVG为例,viewBox尺寸为294×182:
1
2
3
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 294 182">
<!-- ... -->
</svg>
|
我将SVG标签替换为<symbol>,并将其图形添加到我的SVG库中:
1
2
3
4
5
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="quick-draw-hat" viewBox="0 0 294 182">
<g class="quick-draw-hat">[…]</g>
</symbol>
</svg>
|
在多个SVG中使用symbol
我创建了两个SVG:
1
2
3
4
5
6
7
|
<svg class="svg-small" viewBox="0 0 1080 1440">
<!-- ... -->
</svg>
<svg class="svg-large" viewBox="0 0 1920 1080">
<!-- ... -->
</svg>
|
并在两者中插入对我的符号的引用:
1
2
3
4
5
6
7
|
<svg class="svg-small" viewBox="0 0 1080 1440">
<use href="#quick-draw-hat" />
</svg>
<svg class="svg-large" viewBox="0 0 1920 1080">
<use href="#quick-draw-hat" />
</svg>
|
定位符号
一旦使用<use>将符号放入布局中,下一步就是定位它们,这对于为不同屏幕尺寸创建替代布局尤其重要。符号的行为类似于<g>组,因此我可以使用width、height和transform等属性来缩放和移动它们:
1
2
3
4
5
6
7
|
<svg class="svg-small" viewBox="0 0 1080 1440">
<use href="#quick-draw-hat" width="294" height="182" transform="translate(-30,610)"/>
</svg>
<svg class="svg-large" viewBox="0 0 1920 1080">
<use href="#quick-draw-hat" width="294" height="182" transform="translate(350,270)"/>
</svg>
|
动画use元素
我想动画化角色的部分 - 比如Quick Draw的帽子倾斜和他的腿踢动。但是当我添加针对<symbol>内部元素的CSS动画时,什么也没有发生。
提示:您可以动画化<use>元素本身,但不能动画化<symbol>内部的元素。如果您希望单个部分移动,请将它们设为自己的符号并动画化每个<use>。
事实证明,您不能样式化或动画化<symbol>,因为<use>创建了不容易定位的shadow DOM克隆。因此,我必须在每个符号内的库SVG中添加一个<g>元素:
1
2
3
4
5
|
<symbol id="quick-draw-hat" viewBox="0 0 294 182">
<g class="quick-draw-hat">
<!-- ... -->
</g>
</symbol>
|
并使用属性子串选择器动画化它,针对use元素的href属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
use[href="#quick-draw-hat"] {
animation-delay: 0.5s;
animation-direction: alternate;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-name: hat-rock;
animation-timing-function: ease-in-out;
transform-origin: center bottom;
}
@keyframes hat-rock {
from { transform: rotate(-2deg); }
to { transform: rotate(2deg); } }
|
媒体查询显示控制
创建了两个可见的SVG - 一个用于小屏幕,一个用于大屏幕 - 最后一步是决定在哪个屏幕尺寸显示哪个版本。我使用CSS媒体查询来隐藏一个SVG并显示另一个。我默认显示小屏幕SVG:
1
2
|
.svg-small { display: block; }
.svg-large { display: none; }
|
然后我使用min-width媒体查询在64rem及以上切换到大型屏幕SVG:
1
2
3
4
|
@media (min-width: 64rem) {
.svg-small { display: none; }
.svg-large { display: block; }
}
|
总结
通过结合<symbol>、<use>、CSS媒体查询和特定的transform,我可以构建自适应SVG,在不重复内容、加载额外资源或依赖JavaScript的情况下重新定位其元素。我只需要在隐藏的符号库中定义每个图形一次。然后我可以在几个可见的SVG中根据需要重用这些图形。使用CSS进行布局切换,结果是快速且灵活的。
这提醒我们,网络上一些最强大的技术不需要大型框架或复杂的工具 - 只需要一些SVG知识和基础知识的巧妙使用。