You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

292 lines
7.3 KiB

3 weeks ago
  1. <template>
  2. <!-- #ifndef APP-NVUE -->
  3. <view v-show="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick">
  4. <slot></slot>
  5. </view>
  6. <!-- #endif -->
  7. <!-- #ifdef APP-NVUE -->
  8. <view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick">
  9. <slot></slot>
  10. </view>
  11. <!-- #endif -->
  12. </template>
  13. <script>
  14. import { createAnimation } from './createAnimation'
  15. /**
  16. * Transition 过渡动画
  17. * @description 简单过渡动画组件
  18. * @tutorial https://ext.dcloud.net.cn/plugin?id=985
  19. * @property {Boolean} show = [false|true] 控制组件显示或隐藏
  20. * @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
  21. * @value fade 渐隐渐出过渡
  22. * @value slide-top 由上至下过渡
  23. * @value slide-right 由右至左过渡
  24. * @value slide-bottom 由下至上过渡
  25. * @value slide-left 由左至右过渡
  26. * @value zoom-in 由小到大过渡
  27. * @value zoom-out 由大到小过渡
  28. * @property {Number} duration 过渡动画持续时间
  29. * @property {Object} styles 组件样式 css 样式注意带-连接符的属性需要使用小驼峰写法如`backgroundColor:red`
  30. */
  31. export default {
  32. name: 'uniTransition',
  33. emits: ['click', 'change'],
  34. props: {
  35. show: {
  36. type: Boolean,
  37. default: false
  38. },
  39. modeClass: {
  40. type: [Array, String],
  41. default () {
  42. return 'fade'
  43. }
  44. },
  45. duration: {
  46. type: Number,
  47. default: 300
  48. },
  49. styles: {
  50. type: Object,
  51. default () {
  52. return {}
  53. }
  54. },
  55. customClass: {
  56. type: String,
  57. default: ''
  58. },
  59. onceRender: {
  60. type: Boolean,
  61. default: false
  62. },
  63. },
  64. data() {
  65. return {
  66. isShow: false,
  67. transform: '',
  68. opacity: 0,
  69. animationData: {},
  70. durationTime: 300,
  71. config: {}
  72. }
  73. },
  74. watch: {
  75. show: {
  76. handler(newVal) {
  77. if (newVal) {
  78. this.open()
  79. } else {
  80. // 避免上来就执行 close,导致动画错乱
  81. if (this.isShow) {
  82. this.close()
  83. }
  84. }
  85. },
  86. immediate: true
  87. }
  88. },
  89. computed: {
  90. // 生成样式数据
  91. stylesObject() {
  92. let styles = {
  93. ...this.styles,
  94. 'transition-duration': this.duration / 1000 + 's'
  95. }
  96. let transform = ''
  97. for (let i in styles) {
  98. let line = this.toLine(i)
  99. transform += line + ':' + styles[i] + ';'
  100. }
  101. return transform
  102. },
  103. // 初始化动画条件
  104. transformStyles() {
  105. return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
  106. }
  107. },
  108. created() {
  109. // 动画默认配置
  110. this.config = {
  111. duration: this.duration,
  112. timingFunction: 'ease',
  113. transformOrigin: '50% 50%',
  114. delay: 0
  115. }
  116. this.durationTime = this.duration
  117. },
  118. methods: {
  119. /**
  120. * ref 触发 初始化动画
  121. */
  122. init(obj = {}) {
  123. if (obj.duration) {
  124. this.durationTime = obj.duration
  125. }
  126. this.animation = createAnimation(Object.assign(this.config, obj), this)
  127. },
  128. /**
  129. * 点击组件触发回调
  130. */
  131. onClick() {
  132. this.$emit('click', {
  133. detail: this.isShow
  134. })
  135. },
  136. /**
  137. * ref 触发 动画分组
  138. * @param {Object} obj
  139. */
  140. step(obj, config = {}) {
  141. if (!this.animation) return this
  142. Object.keys(obj).forEach(key => {
  143. const value = obj[key]
  144. if (typeof this.animation[key] === 'function') {
  145. Array.isArray(value) ?
  146. this.animation[key](...value) :
  147. this.animation[key](value)
  148. }
  149. })
  150. this.animation.step(config)
  151. return this
  152. },
  153. /**
  154. * ref 触发 执行动画
  155. */
  156. run(fn) {
  157. if (!this.animation) return
  158. this.animation.run(fn)
  159. },
  160. // 开始过度动画
  161. open() {
  162. clearTimeout(this.timer)
  163. this.isShow = true
  164. // 新增初始状态重置逻辑(关键)
  165. this.transform = this.styleInit(false).transform || ''
  166. this.opacity = this.styleInit(false).opacity || 0
  167. // 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
  168. this.$nextTick(() => {
  169. // TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
  170. this.timer = setTimeout(() => {
  171. this.animation = createAnimation(this.config, this)
  172. this.tranfromInit(false).step()
  173. this.animation.run(() => {
  174. // #ifdef APP-NVUE
  175. this.transform = this.styleInit(false).transform || ''
  176. this.opacity = this.styleInit(false).opacity || 1
  177. // #endif
  178. // #ifndef APP-NVUE
  179. this.transform = ''
  180. this.opacity = this.styleInit(false).opacity || 1
  181. // #endif
  182. this.$emit('change', {
  183. detail: this.isShow
  184. })
  185. })
  186. }, 80)
  187. })
  188. },
  189. // 关闭过度动画
  190. close(type) {
  191. if (!this.animation) return
  192. this.tranfromInit(true)
  193. .step()
  194. .run(() => {
  195. this.isShow = false
  196. this.animationData = null
  197. this.animation = null
  198. let { opacity, transform } = this.styleInit(false)
  199. this.opacity = opacity || 1
  200. this.transform = transform
  201. this.$emit('change', {
  202. detail: this.isShow
  203. })
  204. })
  205. },
  206. // 处理动画开始前的默认样式
  207. styleInit(type) {
  208. let styles = { transform: '', opacity: 1 }
  209. const buildStyle = (type, mode) => {
  210. const value = this.animationType(type)[mode] // 直接使用 type 控制状态
  211. if (mode.startsWith('fade')) {
  212. styles.opacity = value
  213. } else {
  214. styles.transform += value + ' '
  215. }
  216. }
  217. if (typeof this.modeClass === 'string') {
  218. buildStyle(type, this.modeClass)
  219. } else {
  220. this.modeClass.forEach(mode => buildStyle(type, mode))
  221. }
  222. return styles
  223. },
  224. // 处理内置组合动画
  225. tranfromInit(type) {
  226. let buildTranfrom = (type, mode) => {
  227. let aniNum = null
  228. if (mode === 'fade') {
  229. aniNum = type ? 0 : 1
  230. } else {
  231. aniNum = type ? '-100%' : '0'
  232. if (mode === 'zoom-in') {
  233. aniNum = type ? 0.8 : 1
  234. }
  235. if (mode === 'zoom-out') {
  236. aniNum = type ? 1.2 : 1
  237. }
  238. if (mode === 'slide-right') {
  239. aniNum = type ? '100%' : '0'
  240. }
  241. if (mode === 'slide-bottom') {
  242. aniNum = type ? '100%' : '0'
  243. }
  244. }
  245. this.animation[this.animationMode()[mode]](aniNum)
  246. }
  247. if (typeof this.modeClass === 'string') {
  248. buildTranfrom(type, this.modeClass)
  249. } else {
  250. this.modeClass.forEach(mode => {
  251. buildTranfrom(type, mode)
  252. })
  253. }
  254. return this.animation
  255. },
  256. animationType(type) {
  257. return {
  258. fade: type ? 1 : 0,
  259. 'slide-top': `translateY(${type ? '0' : '-100%'})`,
  260. 'slide-right': `translateX(${type ? '0' : '100%'})`,
  261. 'slide-bottom': `translateY(${type ? '0' : '100%'})`,
  262. 'slide-left': `translateX(${type ? '0' : '-100%'})`,
  263. 'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
  264. 'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
  265. }
  266. },
  267. // 内置动画类型与实际动画对应字典
  268. animationMode() {
  269. return {
  270. fade: 'opacity',
  271. 'slide-top': 'translateY',
  272. 'slide-right': 'translateX',
  273. 'slide-bottom': 'translateY',
  274. 'slide-left': 'translateX',
  275. 'zoom-in': 'scale',
  276. 'zoom-out': 'scale'
  277. }
  278. },
  279. // 驼峰转中横线
  280. toLine(name) {
  281. return name.replace(/([A-Z])/g, '-$1').toLowerCase()
  282. }
  283. }
  284. }
  285. </script>
  286. <style></style>