uni-transition.vue 6.5 KB

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