uni-transition.vue 7.3 KB

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