uni-transition.vue 6.7 KB

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