uni-transition.vue 6.5 KB

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