SVG创建Material Design波纹效果按钮

本文由码农网 – 小峰原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划

随着Google Material Design的出现,一种旨在跨平台和设备创建统一体验的视觉语言由此横空出世。Google通过“Material Guidelines”动画部分描述的例子是如此地拟真,以致于许多人将这些互动视为Google品牌的一部分。

在本教程中,我们将向大家展示如何在Google Material Design规范的Radial Action下构建波纹效果,并结合SVG和GreenSock功能。

在线演示源码下载

响应式动作

Google使用Radial Action定义Responsive Interaction如下:

Radial action is the visual ripple of ink spreading outward from the point of input.
The connection between an input event and on-screen action should be visually represented to tie them together. For touch or mouse, this occurs at the point of contact. A touch ripple indicates where and when a touch occurs and acknowledges that the touch input was received.
Transitions, or actions triggered by input events, should visually connect to input events. Ripple reactions near the epicenter occur sooner than reactions further away.

Google非常清楚地表述了输入反馈应从原点出发,向外扩散。例如,如果用户直接在中心点击按钮,则纹波将从初始接触点向外扩展。这就是我们如何指出触摸发生的地点和时间的方式,以便向用户确认接收到的输入。

SVG中的径向动作

有许多开发人员创作纹波技术,主要使用CSS技术,如@keyframes,transitions,transforms伪技巧,border-radius以及甚至额外的标记,如span或div。不使用CSS,让我们来看看如何通过GreenSock的TweenMax库用SVG来创建这个径向动作。

创建SVG

不管你信不信,其实我们并不需要如Adobe Illustrator或甚至Sketch这样花哨的应用程序来创作这个效果。SVG的标记可以使用我们可能已经熟悉并用到工作中的几个XML标签来编写。

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewBox="0 0 100 100"></symbol>
</svg>

对于使用SVG精灵图标的用户,你会注意到<symbol>的使用。symbol元素允许在单个symbol实例中匹配相关的XML,并随后实例化它们,或者换句话说——就像盖章一样在整个应用程序中使用它们。每个盖章的实例与其唯一的创建者相同:它所在的symbol。

symbol元素接受诸如viewBox和preserveAspectRatio之类的属性,这些属性可以在引用use元素定义的矩形视口中提供符合缩放比例的能力。Sara Soueidan写了一篇精彩的文章,并建立了一个交互式工具,以帮助你了解viewBox坐标系统。简单地说就是,定义初始的x和y坐标值(0,0),然后定义SVG画布的宽度和高度(100,100)。

这个XML拼图的下一个部分是添加我们打算动画化为波纹的形状。这是放入circle元素的地方。

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewBox="0 0 100 100">
    <circle />
  </symbol>
</svg>

circle需要一些更多的信息,然后它才能在SVG的viewBox内正确地显示。

<circle cx="1" cy="1" r="1"/>

属性cx和cy是相对于SVG viewBox的坐标位置;我们的例子中就是symbol。为了使点击的时候感觉更自然,我们需要确保在接收到输入时触发点直接放在用户手指下方。

上图中间那个例子,其属性创建了一个半径为1px大小为2px × 2px的圆。这将确保我们的圆不会像最后那个示例中所看到的那样裁剪。

<div style="height: 0; width: 0; position: absolute; visibility: hidden;" aria-hidden="true">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false">
    <symbol id="ripply-scott" viewBox="0 0 100 100">
      <circle id="ripple-shape" cx="1" cy="1" r="1"/>
    </symbol>
  </svg>
</div>

对于最后的触摸,我们将用包含内联CSS的div来包装它,以简洁地隐藏sprite。这样可以防止在渲染时占用页面中的空间。

在撰写本文时,SVG精灵包含symbol块引用它自己的渐变定义——正如你在演示中将看到的——通过ID找不到渐变和正确地渲染;使用visibility 属性代替display的原因:none在Firefox和其他大多数浏览器上作为整个渐变都会失败。

所有IE直到IE11都需要使用focusable=”false” ;除了Edge,因为它还没有测试过。这是来自SVG 1.2规范的一个提案,描述了键盘焦点控制应该如何工作。IE实现了这一点,其他的浏览器则不行。为了与HTML一致,并且为了更好的控制,SVG 2将转而采用tabindex。

编写标记

让我们写一个语义的button元素作为我们的对象,以显示此波纹。

<button>Click for Ripple</button>

大多数我们熟悉的button的标记结构是直截了当的,包括一些填充文本。

<button>
  Click for Ripple
  <svg>
    <use xlink:href="#ripply-scott"></use>
  </svg>
</button>

为了利用先前创建的symbol元素,我们需要方法来引用它,通过使用按钮的SVG中的use元素来引用符号的ID属性值。

<button id="js-ripple-btn" class="button styl-material">
  Click for Ripple
  <svg class="ripple-obj" id="js-ripple">
    <use width="100" height="100" xlink:href="#ripply-scott" class="js-ripple"></use>
  </svg>
</button>

最终标记具备了CSS和JavaScript hooks的附加属性。以“js-”开头的属性值表示仅存在于JavaScript中的值,因此删除它们将阻碍交互,但不会影响样式。这有助于区分CSS选择器和JavaScript hooks,以避免在将来需要删除或更新时相互混淆。

use元素必须有定义的宽度和高度,否则将不会对查看者可见。你也可以在CSS中定义,如果你直接在元素本身上决定不要的话。

联结点样式

当编写CSS的时候,要达到预期的效果你所要做的并不多。

.ripple-obj {
  height: 100%;
  pointer-events: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 0;
  fill: #0c7cd5;
}

.ripple-obj use {
  opacity: 0;
}

这就是在删除用于一般样式的声明时,还留下的内容。pointer-events的使用消除了SVG纹波成为鼠标事件的目标,因为我们只需要父对象反应:button元素。

纹波最初必须是不可见的,因此要将不透明度值设置为零。我们还将波纹对象定位在button的左上方。我们可以使波纹形状居中,但是由于此事件是基于用户交互而发生的,所以担心位置没有意义。

赋予它生机

赋予生机正是这个互动所有的意义。

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<script src="js/ripple.js"></script>

为了动画化波纹,我们将使用GreenSock的TweenMax库,因为它是使用JavaScript对对象进行动画处理的最佳库之一;特别是涉及与动画SVG跨浏览器有关的问题。

var ripplyScott = (function() {}
  return {
    init: function() {}
  };
})();

我们将要使用的模式是所谓的模块模式,因为它有助于隐藏和保护全局命名空间。

var ripplyScott = (function() {}
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {…}
})();

为了解决问题,我们将抓取一些元素并将它们存储在变量中;特别是use元素,它包含button内的svg。整个动画逻辑将驻留在rippleAnimation函数中。该函数将接受动画序列和事件信息的时序参数。

var ripplyScott = (function() {}
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) - x ),
        offsetY      = Math.abs( (h / 2) - y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
  }
})();

我们定义了大量的变量,所以让我们一个一个地讨论这些变量所负责的内容。

var tl = new TimelineMax();

此变量创建动画序列的时间轴实例以及所有时间轴在TweenMax中实例化的方式。

var x = event.offsetX;
var y = event.offsetY;

事件偏移量是一个只读属性,它将鼠标指针的偏移值报告给目标节点的填充边。在这个例子中,就是我们的button。x的事件偏移量从左到右计算,y的事件偏移量从上到下计算;都从零开始。

var w = event.target.offsetWidth;
var h = event.target.offsetHeight;

这些变量将返回按钮的宽度和高度。最终计算结果将包括元素边框和填充的大小。我们需要这个值才能知道我们的元素有多大,这样我们才可以将波纹传播到最远的边缘。

var offsetX = Math.abs( (w / 2) - x );
var offsetY = Math.abs( (h / 2) - y );

偏移值是点击距离元素中心的偏移距离。为了填满目标的整个区域,波纹必须足够大,可以从接触点覆盖到最远的角落。使用初始x和y坐标将不会再次将其从零开始,对于x,是从左到右的值,对于y,是从上到下的值。这种方法让我们使用这些值的时候无论目标的中心点点击在哪一边,都会检测距离。

注意圆将如何覆盖整个元素的过程,无论输入的起始点何处发生。根据起始点的交互来覆盖整个表面,我们需要做一些数学。

以下是我们如何使用464 x 82作为宽和高,391和45作为x和y坐标来计算偏移量的过程:

var offsetX = (464 / 2) - 391 = -159
var offsetY = (82 / 2) - 45 = -4

通过将宽度和高度除以2来找到中心,然后减去由x和y坐标检测到的报告值。

Math.abs()方法返回数字的绝对值。使用上面的算术得到值159和4。

var deltaX  = 232 + 159 = 391;
var deltaY  = 41 + 4 = 45;

三角计算点击的整个距离,而不是距离中心的距离。选择三角的原因是x和y总是从零开始从左到右,所以当相反方向(从右到左)点击的时候,我们需要方法来检测点击。

学过基础数学课程的小伙伴应该都知道勾股定理。公式为:高(a)的平方加底(b)的平方,得到斜边(c)的平方。

a2 + b2 = c2

var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

使用这个公式让我们来看一下计算:

var scale_ratio = Math.sqrt(Math.pow(391, 2) + Math.pow(45, 2));

Math.pow()方法返回第一个参数的幂;在这个例子中增加了一倍。391的2次方为152881。后面45的2次方等于2025。将这两个值相加并取结果的平方根将留下393.58099547615353,这就是我们需要的波纹比例。

var ripplyScott = (function() {
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) - x ),
        offsetY      = Math.abs( (h / 2) - y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

    tl.fromTo(ripple, timing, {
      x: x,
      y: y,
      transformOrigin: '50% 50%',
      scale: 0,
      opacity: 1,
      ease: Linear.easeIn
    },{
      scale: scale_ratio,
      opacity: 0
    });

    return tl;
  }
})();

使用TweenMax中的fromTo方法,我们可以传递目标——波纹形状——并设置包含整个运动序列方向的对象文字。鉴于我们想要从中心向外形成动画,SVG需要将转换原点设置为中间位置。考虑到我们想要之后要进行动画处理,需要设置opacity 为1,因此缩放也需要调整到最小的位置。不知道你回想起了没有,之前我们在CSS中设置了opacity为0的use元素以及我们从值1开始并返回到零的原因。最后部分是返回时间轴实例。

var ripplyScott = (function() {
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) - x ),
        offsetY      = Math.abs( (h / 2) - y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

    tl.fromTo(ripple, timing, {
      x: x,
      y: y,
      transformOrigin: '50% 50%',
      scale: 0,
      opacity: 1,
      ease: Linear.easeIn
    },{
      scale: scale_ratio,
      opacity: 0
    });

    return tl;
  }

  return {
    init: function(target, timing) {
      var button = document.getElementById(target);

      button.addEventListener('click', function(event) {
        rippleAnimation.call(this, event, timing);
      });
    }
  };
})();

返回的对象字面值将控制我们的波纹,方法是通过将事件侦听器附加到所需的目标,调用rippleAnimation,以及最后传递我们将在下一步讨论的参数。

ripplyScott.init('js-ripple-btn', 0.75);

最后通过使用模块并传递init函数来对按钮进行调用,init函数传递按钮和序列的时序。看,就是这样!

希望你喜欢这篇文章,并从中受到启迪!欢迎使用不同的形状来检查演示,并查看源代码。不妨尝试新的形状、新的图层形状,最重要的是发挥你的想象力,放飞你的创意。

注意:其中一些技术是试验性的,只能在现代浏览器中运行。

浏览器支持:Chrome Firefox Internet Explorer Safari Opera

Github上查看这个项目

译文链接:http://www.codeceo.com/article/svg-material-design-ripple-button.html
英文原文:Creating Material Design Ripple Effects with SVG
翻译作者:码农网 – 小峰
转载必须在正文中标注并保留原文链接、译文链接和译者等信息。]