uni-transition.vue 6.4 KB

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