SVG 创建 Material Design 波纹效果按钮

FreddieNzx 4年前
   <p>随着Google Material Design的出现,一种旨在跨平台和设备创建统一体验的视觉语言由此横空出世。Google通过“Material Guidelines”动画部分描述的例子是如此地拟真,以致于许多人将这些互动视为Google品牌的一部分。</p>    <p>在本教程中,我们将向大家展示如何在Google Material Design规范的 <a href="/misc/goto?guid=4959755032306569471" rel="nofollow,noindex">Radial Action</a> 下构建波纹效果,并结合SVG和GreenSock功能。</p>    <p><img src="https://simg.open-open.com/show/9805dbd923343fa8c2a496624344f86a.jpg"></p>    <p><a href="/misc/goto?guid=4959755032400690874" rel="nofollow,noindex">在线演示</a> <a href="/misc/goto?guid=4959755032486550195" rel="nofollow,noindex">源码下载</a></p>    <h2>响应式动作</h2>    <p>Google使用Radial Action定义Responsive Interaction如下:</p>    <p>Radial action is the visual ripple of ink spreading outward from the point of input.</p>    <p>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.</p>    <p>Transitions, or actions triggered by input events, should visually connect to input events. Ripple reactions near the epicenter occur sooner than reactions further away.</p>    <p>Google非常清楚地表述了输入反馈应从原点出发,向外扩散。例如,如果用户直接在中心点击按钮,则纹波将从初始接触点向外扩展。这就是我们如何指出触摸发生的地点和时间的方式,以便向用户确认接收到的输入。</p>    <h2>SVG中的径向动作</h2>    <p>有许多开发人员创作纹波技术,主要使用CSS技术,如@keyframes,transitions,transforms伪技巧,border-radius以及甚至额外的标记,如span或div。不使用CSS,让我们来看看如何通过GreenSock的TweenMax库用SVG来创建这个径向动作。</p>    <h3>创建SVG</h3>    <p>不管你信不信,其实我们并不需要如Adobe Illustrator或甚至Sketch这样花哨的应用程序来创作这个效果。SVG的标记可以使用我们可能已经熟悉并用到工作中的几个XML标签来编写。</p>    <pre>  <code class="language-javascript"><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"/>  </svg>  </code></pre>    <p>对于使用SVG精灵图标的用户,你会注意到的使用。symbol元素允许在单个symbol实例中匹配相关的XML,并随后实例化它们,或者换句话说——就像盖章一样在整个应用程序中使用它们。每个盖章的实例与其唯一的创建者相同:它所在的symbol。</p>    <p>symbol元素接受诸如viewBox和preserveAspectRatio之类的属性,这些属性可以在引用use元素定义的矩形视口中提供符合缩放比例的能力。Sara Soueidan写了一篇精彩的文章,并建立了一个交互式工具,以帮助你了解viewBox坐标系统。简单地说就是,定义初始的x和y坐标值(0,0),然后定义SVG画布的宽度和高度(100,100)。</p>    <p>这个XML拼图的下一个部分是添加我们打算动画化为波纹的形状。这是放入circle元素的地方。</p>    <pre>  <code class="language-javascript"><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>  </code></pre>    <p>circle需要一些更多的信息,然后它才能在SVG的viewBox内正确地显示。</p>    <pre>  <code class="language-javascript"><circle cx="1" cy="1" r="1"/>  </code></pre>    <p>属性cx和cy是相对于SVG viewBox的坐标位置;我们的例子中就是symbol。为了使点击的时候感觉更自然,我们需要确保在接收到输入时触发点直接放在用户手指下方。</p>    <p><img src="https://simg.open-open.com/show/c9e202c39b1a85b0775892f004e4b1b2.png"></p>    <p>上图中间那个例子,其属性创建了一个半径为1px大小为2px × 2px的圆。这将确保我们的圆不会像最后那个示例中所看到的那样裁剪。</p>    <pre>  <code class="language-javascript"><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>  </code></pre>    <p>对于最后的触摸,我们将用包含内联CSS的div来包装它,以简洁地隐藏sprite。这样可以防止在渲染时占用页面中的空间。</p>    <p>在撰写本文时,SVG精灵包含symbol块引用它自己的渐变定义——正如你在演示中将看到的——通过ID找不到渐变和正确地渲染;使用visibility 属性代替display的原因:none在Firefox和其他大多数浏览器上作为整个渐变都会失败。</p>    <p>所有IE直到IE11都需要使用focusable=”false” ;除了Edge,因为它还没有测试过。这是来自SVG 1.2规范的一个提案,描述了键盘焦点控制应该如何工作。IE实现了这一点,其他的浏览器则不行。为了与HTML一致,并且为了更好的控制,SVG 2将转而采用tabindex。</p>    <h2>编写标记</h2>    <p>让我们写一个语义的button元素作为我们的对象,以显示此波纹。</p>    <pre>  <code class="language-javascript"><span class="hljs-tag"><<span class="hljs-name">button</span>></span>Click for Ripple<span class="hljs-tag"></<span class="hljs-name">button</span>></span>  </code></pre>    <p>大多数我们熟悉的button的标记结构是直截了当的,包括一些填充文本。</p>    <pre>  <code class="language-javascript"><span class="hljs-tag"><<span class="hljs-name">button</span>></span>    Click for Ripple    <span class="hljs-tag"><<span class="hljs-name">svg</span>></span>      <span class="hljs-tag"><<span class="hljs-name">use</span> <span class="hljs-attr">xlink:href</span>=<span class="hljs-string">"#ripply-scott"</span>></span><span class="hljs-tag"></<span class="hljs-name">use</span>></span>    <span class="hljs-tag"></<span class="hljs-name">svg</span>></span>  <span class="hljs-tag"></<span class="hljs-name">button</span>></span>  </code></pre>    <p>为了利用先前创建的symbol元素,我们需要方法来引用它,通过使用按钮的SVG中的use元素来引用符号的ID属性值。</p>    <pre>  <code class="language-javascript"><button id=<span class="hljs-string">"js-ripple-btn"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"button styl-material"</span>>    Click <span class="hljs-keyword">for</span> Ripple    <svg <span class="hljs-keyword">class</span>=<span class="hljs-string">"ripple-obj"</span> id=<span class="hljs-string">"js-ripple"</span>>      <use width=<span class="hljs-string">"100"</span> height=<span class="hljs-string">"100"</span> xlink:href=<span class="hljs-string">"#ripply-scott"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"js-ripple"</span>></use>    </svg>  </button>  </code></pre>    <p>最终标记具备了CSS和JavaScript hooks的附加属性。以“js-”开头的属性值表示仅存在于JavaScript中的值,因此删除它们将阻碍交互,但不会影响样式。这有助于区分CSS选择器和JavaScript hooks,以避免在将来需要删除或更新时相互混淆。</p>    <p>use元素必须有定义的宽度和高度,否则将不会对查看者可见。你也可以在CSS中定义,如果你直接在元素本身上决定不要的话。</p>    <h2>联结点样式</h2>    <p>当编写CSS的时候,要达到预期的效果你所要做的并不多。</p>    <pre>  <code class="language-javascript">.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;  }  </code></pre>    <p>这就是在删除用于一般样式的声明时,还留下的内容。pointer-events的使用消除了SVG纹波成为鼠标事件的目标,因为我们只需要父对象反应:button元素。</p>    <p>纹波最初必须是不可见的,因此要将不透明度值设置为零。我们还将波纹对象定位在button的左上方。我们可以使波纹形状居中,但是由于此事件是基于用户交互而发生的,所以担心位置没有意义。</p>    <h2>赋予它生机</h2>    <p>赋予生机正是这个互动所有的意义。</p>    <pre>  <code class="language-javascript"><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"/>  <script src="js/ripple.js"/>  </code></pre>    <p>为了动画化波纹,我们将使用GreenSock的TweenMax库,因为它是使用JavaScript对对象进行动画处理的最佳库之一;特别是涉及与动画SVG跨浏览器有关的问题。</p>    <pre>  <code class="language-javascript">var ripplyScott = (function() {}    return {      init: function() {}    };  })();  </code></pre>    <p>我们将要使用的模式是所谓的模块模式,因为它有助于隐藏和保护全局命名空间。</p>    <pre>  <code class="language-javascript">var ripplyScott = (function() {}    var circle = document.getElementById('js-ripple'),        ripple = document.querySelectorAll('.js-ripple');       function rippleAnimation(event, timing) {…}  })();  </code></pre>    <p>为了解决问题,我们将抓取一些元素并将它们存储在变量中;特别是use元素,它包含button内的svg。整个动画逻辑将驻留在rippleAnimation函数中。该函数将接受动画序列和事件信息的时序参数。</p>    <pre>  <code class="language-javascript">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));    }  })();  </code></pre>    <p>我们定义了大量的变量,所以让我们一个一个地讨论这些变量所负责的内容。</p>    <pre>  <code class="language-javascript">var tl = new TimelineMax();  </code></pre>    <p>此变量创建动画序列的时间轴实例以及所有时间轴在TweenMax中实例化的方式。</p>    <pre>  <code class="language-javascript">var x = event.offsetX;  var y = event.offsetY;  </code></pre>    <p>事件偏移量是一个只读属性,它将鼠标指针的偏移值报告给目标节点的填充边。在这个例子中,就是我们的button。x的事件偏移量从左到右计算,y的事件偏移量从上到下计算;都从零开始。</p>    <pre>  <code class="language-javascript">var w = event.target.offsetWidth;  var h = event.target.offsetHeight;  </code></pre>    <p>这些变量将返回按钮的宽度和高度。最终计算结果将包括元素边框和填充的大小。我们需要这个值才能知道我们的元素有多大,这样我们才可以将波纹传播到最远的边缘。</p>    <pre>  <code class="language-javascript">var offsetX = Math.abs( (w / 2) - x );  var offsetY = Math.abs( (h / 2) - y );  </code></pre>    <p>偏移值是点击距离元素中心的偏移距离。为了填满目标的整个区域,波纹必须足够大,可以从接触点覆盖到最远的角落。使用初始x和y坐标将不会再次将其从零开始,对于x,是从左到右的值,对于y,是从上到下的值。这种方法让我们使用这些值的时候无论目标的中心点点击在哪一边,都会检测距离。</p>    <p><img src="https://simg.open-open.com/show/81ee7e555041659d2196a6a8a3a010b6.gif"></p>    <p>注意圆将如何覆盖整个元素的过程,无论输入的起始点何处发生。根据起始点的交互来覆盖整个表面,我们需要做一些数学。</p>    <p>以下是我们如何使用464 x 82作为宽和高,391和45作为x和y坐标来计算偏移量的过程:</p>    <pre>  <code class="language-javascript">var offsetX = (464 / 2) - 391 = -159  var offsetY = (82 / 2) - 45 = -4  </code></pre>    <p>通过将宽度和高度除以2来找到中心,然后减去由x和y坐标检测到的报告值。</p>    <p>Math.abs()方法返回数字的绝对值。使用上面的算术得到值159和4。</p>    <pre>  <code class="language-javascript">var deltaX  = 232 + 159 = 391;  var deltaY  = 41 + 4 = 45;  </code></pre>    <p>三角计算点击的整个距离,而不是距离中心的距离。选择三角的原因是x和y总是从零开始从左到右,所以当相反方向(从右到左)点击的时候,我们需要方法来检测点击。</p>    <p><img src="https://simg.open-open.com/show/3e389b89cf03205f04a255abf7614bfd.png"></p>    <p>学过基础数学课程的小伙伴应该都知道勾股定理。公式为:高(a)的平方加底(b)的平方,得到斜边(c)的平方。</p>    <p>a <sup>2</sup> + b <sup>2</sup> = c <sup>2</sup></p>    <pre>  <code class="language-javascript">var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));  </code></pre>    <p>使用这个公式让我们来看一下计算:</p>    <pre>  <code class="language-javascript">var scale_ratio = Math.sqrt(Math.pow(391, 2) + Math.pow(45, 2));  </code></pre>    <p>Math.pow()方法返回第一个参数的幂;在这个例子中增加了一倍。391的2次方为152881。后面45的2次方等于2025。将这两个值相加并取结果的平方根将留下393.58099547615353,这就是我们需要的波纹比例。</p>    <pre>  <code class="language-javascript">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;    }  })();  </code></pre>    <p>使用TweenMax中的fromTo方法,我们可以传递目标——波纹形状——并设置包含整个运动序列方向的对象文字。鉴于我们想要从中心向外形成动画,SVG需要将转换原点设置为中间位置。考虑到我们想要之后要进行动画处理,需要设置opacity 为1,因此缩放也需要调整到最小的位置。不知道你回想起了没有,之前我们在CSS中设置了opacity为0的use元素以及我们从值1开始并返回到零的原因。最后部分是返回时间轴实例。</p>    <pre>  <code class="language-javascript">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);        });      }    };  })();  </code></pre>    <p>返回的对象字面值将控制我们的波纹,方法是通过将事件侦听器附加到所需的目标,调用rippleAnimation,以及最后传递我们将在下一步讨论的参数。</p>    <pre>  <code class="language-javascript">ripplyScott.init('js-ripple-btn', 0.75);  </code></pre>    <p>最后通过使用模块并传递init函数来对按钮进行调用,init函数传递按钮和序列的时序。看,就是这样!</p>    <p>希望你喜欢这篇文章,并从中受到启迪!欢迎使用不同的形状来检查演示,并查看源代码。不妨尝试新的形状、新的图层形状,最重要的是发挥你的想象力,放飞你的创意。</p>    <p>注意:其中一些技术是试验性的,只能在现代浏览器中运行。</p>    <p>浏览器支持:Chrome Firefox Internet Explorer Safari Opera</p>    <p>来自:http://web.jobbole.com/92742/</p>    <p> </p>