自定义事件 水波纹效果vue原生

这个交互有点意思

基本逻辑

确定作用目标范围

水波纹指令需要作用在一个块级元素,元素要有宽度和高度,每当用户点击,获取用户点击坐标到块级元素的四个直角顶点的距离其中距离最远的顶点作为ripple半径画圆
image.png

确定交互事件-pointEvent

交互事件对于ripple的生成与移除,我们使用pointEvent指针事件

指针事件 - Pointer events 是一类可以为定点设备所触发的DOM事件。它们被用来创建一个可以有效掌握各类输入设备(鼠标、触控笔和单点或多点的手指触摸)的统一的DOM事件模型。 所谓指针 是指一个可以明确指向屏幕上某一组坐标的硬件设备。建立这样一个单独的事件模型可以有效的简化Web站点与应用所需的工作,同时也便于提供更加一致与良好的用户体验,无需关心不同用户和场景在输入硬件上的差异。而且,对于某些特定设备才可处理的交互情景,指针事件也定义了一个 pointerType 属性以使开发者可以知晓该事件的触发设备,以有针对性的加以处理。 也就是说本来web端我们会使用mousedown、mouseover、mouseup等来监听鼠标时间、移动端我们会用touchstarttouchmove、touchend监听触摸时间,但是使用了指针事件Pointer events 就不用这样区分了,它会自动兼容web端还是移动端的事件,也会返回pointerType属性表明触发设备。

元素生成效果

因为水波纹实际上是一个圆形,最终实现的大小会比当前点击元素大很多

原生交互效果

点击查看【juejin】

自定义事件封装 v-ripple

之前的实现用的over-flow: “hidden” 一种解决办法是我们给父元素添加over-flow: “hidden”,但是这样也会存在着一个问题,我们给父元素动态添加css属性属于修改了用户层面的样式,这就会导致各种隐藏bug,引起不必要的麻烦 这种解决方法,我们采用父元素与ripple之间在嵌套一层元素,作为ripple的父元素,并且对最外层的父元素也就是用户点击的元素做绝对定位就可以解决问题

什么是自定义事件

设置配置项

  1. //自定义指令的总配置属性
  2. interface IRippleDirectiveOptions {
  3. /**
  4. *
  5. * @remarks
  6. * Y* 你可以设置 ·currentColor· to 能够自动使用元素的文本颜色
  7. *
  8. * @default
  9. * 'currentColor'
  10. */
  11. color: string
  12. /**
  13. * 第一次出现的透明度
  14. *
  15. * @default
  16. * 0.2 默认opacity 0.2
  17. */
  18. initialOpacity: number
  19. /**
  20. * 在透明度 结束的时候 stopped 的时候 我们设置透明度的大小
  21. *
  22. * @default
  23. * 0.1
  24. */
  25. finalOpacity: number
  26. /**
  27. * 动画持续事件
  28. *
  29. * @default
  30. * 400ms
  31. */
  32. duration: number
  33. /**
  34. * css 动画 从开始到结束 以相同的时间来执行动画
  35. *
  36. * @default
  37. * 'ease-out'
  38. */
  39. easing: string
  40. /**
  41. * 取消延迟时间
  42. *
  43. * @note
  44. * 类似于 debounceTime
  45. * @default
  46. * 75ms
  47. */
  48. delay: number
  49. /**
  50. * 禁止 水波
  51. *
  52. * @note
  53. * 类似于 点击禁止ripple效果
  54. * @default
  55. * false
  56. */
  57. disabled: boolean
  58. /**
  59. * ripple展示方式
  60. *
  61. * @note
  62. * ripple在center中间开始动画
  63. * @default
  64. * false
  65. */
  66. center: boolean
  67. }
  68. //辅助补充配置
  69. interface IRipplePluginOptions extends IRippleDirectiveOptions {
  70. /**
  71. * 用于覆盖指令的名称
  72. * 默认指令 ripple
  73. */
  74. directive: string
  75. }
  76. // 给可预见值 value 添加类型
  77. interface IRippleDirectiveOptionWithBinding {
  78. value: IRippleDirectiveOptions
  79. }
  80. interface HTMLElementRectType {
  81. width: number
  82. height: number
  83. top: number
  84. left: number
  85. bottom: number
  86. right: number
  87. }
  88. // 初始化配置,default_plugin_options
  89. const DEFAULT_PLUGIN_OPTIONS: IRipplePluginOptions = {
  90. directive: 'ripple',
  91. color: 'currentColor', // 可选,默认当前文本颜色
  92. initialOpacity: 0.1, //可选,初始交互效果透明度大小
  93. finalOpacity: 0.2, //可选,结束交互效果长按透明度大小
  94. duration: 350, // 可选,持续时间
  95. easing: 'ease-out', //可选,缓动动画
  96. delay: 60, // 可选,延迟 debouceTime 时间后调用
  97. disabled: false, // 可选,禁止水波效果
  98. center: false // 是否从中间开始
  99. }
  100. export {
  101. DEFAULT_PLUGIN_OPTIONS,
  102. IRipplePluginOptions,
  103. HTMLElementRectType,
  104. IRippleDirectiveOptions,
  105. IRippleDirectiveOptionWithBinding
  106. }

创建水波纹父组件容器

  1. export const createContainer = ({
  2. borderTopLeftRadius,
  3. borderTopRightRadius,
  4. borderBottomLeftRadius,
  5. borderBottomRightRadius
  6. }: CSSStyleDeclaration): HTMLElement => {
  7. const rippleContainer = document.createElement('div')
  8. rippleContainer.style.top = '0'
  9. rippleContainer.style.left = '0'
  10. rippleContainer.style.width = '100%'
  11. rippleContainer.style.height = '100%'
  12. rippleContainer.style.position = 'absolute'
  13. rippleContainer.style.borderRadius = `${borderTopLeftRadius} ${borderTopRightRadius} ${borderBottomRightRadius} ${borderBottomLeftRadius}`
  14. rippleContainer.style.overflow = 'hidden'
  15. rippleContainer.style.pointerEvents = 'none'
  16. // 兼容 ie 苹果
  17. rippleContainer.style.webkitMaskImage =
  18. '-webkit-radial-gradient(white, black)'
  19. return rippleContainer
  20. }

设置水波纹子组件

  1. import { IRippleDirectiveOptions, HTMLElementRectType } from '../options'
  2. export const createRippleElement = (
  3. x: number,
  4. y: number,
  5. size: number,
  6. options: IRippleDirectiveOptions,
  7. rect: HTMLElementRectType
  8. ): HTMLElement => {
  9. const rippleElement = document.createElement('div')
  10. rippleElement.style.position = 'absolute'
  11. rippleElement.style.width = options.center
  12. ? `${Math.sqrt(rect.width * rect.width + rect.height * rect.height)}px`
  13. : `${size * 2}px`
  14. // rippleElement.style.height = options.center ? `${size}px` : `${size}px`;
  15. rippleElement.style.height = options.center
  16. ? `${Math.sqrt(rect.width * rect.width + rect.height * rect.height)}px`
  17. : `${size * 2}px`
  18. rippleElement.style.top = options.center ? `${rect.height / 2}px` : `${y}px`
  19. rippleElement.style.left = options.center ? `${rect.width / 2}px` : `${x}px`
  20. rippleElement.style.background = options.color
  21. rippleElement.style.borderRadius = '50%'
  22. rippleElement.style.opacity = `${options.initialOpacity}`
  23. rippleElement.style.transform = `translate(-50%,-50%) scale(0)`
  24. rippleElement.style.transition = `transform ${
  25. options.duration / 1000
  26. }s cubic-bezier(0, 0.5, 0.25, 1)
  27. , opacity ${options.duration / 1000}s
  28. cubic-bezier(0.0, 0, 0.2, 1)
  29. `
  30. return rippleElement
  31. }
  32. // Standard 默认效果
  33. // cubic-bezier(0.0, 0, 0.2, 1)

获取作用源圈的半径

  1. // 毕达哥拉斯定理
  2. export function getPythagoreanDistance(
  3. x1: number,
  4. y1: number,
  5. x2: number,
  6. y2: number
  7. ): number {
  8. const deltaX = x1 - x2
  9. const deltaY = y1 - y2
  10. return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
  11. }
  12. interface DOMRectValue {
  13. x: number
  14. y: number
  15. diameter: number
  16. }
  17. // 获取点击目标的位置到块级作用域直线边界的距离
  18. export function getDistanceToFurthestCorner(
  19. event: PointerEvent,
  20. { width, height, left, top }: DOMRect
  21. ): DOMRectValue {
  22. const x = event.clientX - left
  23. const y = event.clientY - top
  24. const topLeft = getPythagoreanDistance(x, y, 0, 0)
  25. const topRight = getPythagoreanDistance(x, y, width, 0)
  26. const bottomLeft = getPythagoreanDistance(x, y, 0, height)
  27. const bottomRight = getPythagoreanDistance(x, y, width, height)
  28. const diameter = Math.max(topLeft, topRight, bottomLeft, bottomRight)
  29. return {
  30. x,
  31. y,
  32. diameter
  33. }
  34. }

配置入口

  1. /*
  2. * @Date: 2022-03-29 11:33:22
  3. * @LastEditTime: 2022-09-04 10:19:15
  4. * @Author:webkubor
  5. */
  6. import {
  7. DEFAULT_PLUGIN_OPTIONS,
  8. IRippleDirectiveOptions,
  9. IRippleDirectiveOptionWithBinding
  10. } from './options'
  11. import { getHooks } from './utils/hooks'
  12. import { ripple } from './v-ripple'
  13. const optionMap = new WeakMap<
  14. HTMLElement,
  15. Partial<IRippleDirectiveOptions> | false
  16. >()
  17. const globalOptions = { ...DEFAULT_PLUGIN_OPTIONS }
  18. export default {
  19. install: (app) => {
  20. const hooks = getHooks(app)
  21. app.directive('ripple', {
  22. [hooks.mounted](
  23. el: HTMLElement,
  24. binding: IRippleDirectiveOptionWithBinding
  25. ) {
  26. optionMap.set(el, binding.value ?? {})
  27. el.addEventListener('pointerdown', (event) => {
  28. const options = optionMap.get(el)
  29. if (binding.value?.disabled) return
  30. if (options === false) return
  31. ripple(event, el, {
  32. ...globalOptions,
  33. ...options
  34. })
  35. })
  36. },
  37. [hooks.updated](
  38. el: HTMLElement,
  39. binding: IRippleDirectiveOptionWithBinding
  40. ) {
  41. optionMap.set(el, binding.value ?? {})
  42. }
  43. })
  44. }
  45. }