uv-transition.vue 7.9 KB

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