uni-transition.vue 6.7 KB

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