[Improvement] Tab animation fluency && position (#379)

This commit is contained in:
neverland
2017-12-05 20:23:34 +08:00
committed by GitHub
parent 2327e75516
commit 5a17bc520a
12 changed files with 199 additions and 393 deletions
+118 -200
View File
@@ -1,39 +1,23 @@
<template>
<div class="van-tabs" :class="`van-tabs--${type}`">
<div class="van-tabs__nav-wrap" v-if="type === 'line' && tabs.length > swipeThreshold">
<div class="van-tabs__swipe" ref="swipe">
<div class="van-tabs__nav van-tabs__nav--line">
<div class="van-tabs__nav-bar" :style="navBarStyle"></div>
<div
v-for="(tab, index) in tabs"
:key="index"
class="van-tab van-hairline"
:class="{ 'van-tab--active': index === curActive }"
ref="tabkey"
@click="handleTabClick(index)"
>
<span>{{ tab.title }}</span>
</div>
<div :class="{ 'van-tabs__swipe': scrollable, 'van-hairline--top-bottom': type === 'line' }">
<div class="van-tabs__nav" :class="`van-tabs__nav--${type}`" ref="nav">
<div v-if="type === 'line'" class="van-tabs__nav-bar" :style="navBarStyle" />
<div
v-for="(tab, index) in tabs"
:key="index"
ref="tabs"
class="van-tab"
:class="{
'van-tab--active': index === curActive,
'van-tab--disabled': tab.disabled
}"
@click="onClick(index)"
>
<span>{{ tab.title }}</span>
</div>
</div>
</div>
<div
v-else
class="van-tabs__nav"
:class="`van-tabs__nav--${type}`"
>
<div class="van-tabs__nav-bar" :style="navBarStyle" v-if="type === 'line'"></div>
<div
v-for="(tab, index) in tabs"
:key="index"
class="van-tab van-hairline"
:class="{ 'van-tab--active': index === curActive }"
ref="tabkey"
@click="handleTabClick(index)"
>
<span>{{ tab.title }}</span>
</div>
</div>
<div class="van-tabs__content">
<slot></slot>
</div>
@@ -41,192 +25,126 @@
</template>
<script>
import swipe from './swipe';
import translateUtil from '../utils/transition';
import { raf } from '../utils/raf';
export default {
name: 'van-tabs',
export default {
name: 'van-tabs',
props: {
// 外部传入的激活的tab标签
active: {
type: [Number, String],
default: 0
},
// 是默认的line还是card
type: {
type: String,
default: 'line'
},
// 切换tab的动画时间
duration: {
type: Number,
default: 0.3
},
swipeThreshold: {
type: Number,
default: 4
}
props: {
active: {
type: [Number, String],
default: 0
},
type: {
type: String,
default: 'line'
},
duration: {
type: Number,
default: 0.2
},
swipeThreshold: {
type: Number,
default: 4
}
},
data() {
this.winWidth = this.$isServer ? 0 : window.innerWidth;
return {
tabs: [],
curActive: 0,
navBarStyle: {}
};
},
watch: {
active(val) {
this.correctActive(val);
},
data() {
return {
tabs: [],
curActive: +this.active,
isSwiping: false,
isInitEvents: false,
navBarStyle: {}
};
tabs(tabs) {
this.correctActive(this.curActive);
this.setNavBar();
},
watch: {
active(val) {
this.curActive = +val;
},
curActive() {
this.scrollIntoView();
this.setNavBar();
}
},
curActive() {
this.setNavBarStyle();
/* istanbul ignore else */
if (this.tabs.length > this.swipeThreshold) {
this.doOnValueChange();
}
},
tabs(val) {
this.$nextTick(() => {
this.setNavBarStyle();
if (val.length > this.swipeThreshold) {
this.initEvents();
this.doOnValueChange();
} else {
this.isInitEvents = false;
}
mounted() {
this.correctActive(this.active);
this.setNavBar();
},
const activeExist = val.some(tab => tab.index === this.curActive);
if (!activeExist) {
this.curActive = val[0].index || 0;
}
});
}
},
computed: {
scrollable() {
return this.tabs.length > this.swipeThreshold;
}
},
computed: {
swipeWidth() {
return this.$refs.swipe && this.$refs.swipe.getBoundingClientRect().width;
},
maxTranslate() {
/* istanbul ignore if */
if (!this.$refs.tabkey) return;
const lastTab = this.$refs.tabkey[this.tabs.length - 1];
const lastTabWidth = lastTab.offsetWidth;
const lastTabOffsetLeft = lastTab.offsetLeft;
return (lastTabOffsetLeft + lastTabWidth) - this.swipeWidth;
}
},
mounted() {
// 页面载入完成
methods: {
setNavBar() {
this.$nextTick(() => {
this.setNavBarStyle();
if (this.tabs.length > this.swipeThreshold) {
this.initEvents();
this.doOnValueChange();
const tab = this.$refs.tabs[this.curActive];
this.navBarStyle = {
width: `${tab.offsetWidth || 0}px`,
transform: `translate3d(${tab.offsetLeft || 0}px, 0, 0)`,
transitionDuration: `${this.duration}s`
}
});
},
methods: {
/**
* `type`为`line`时,tab下方的横线的样式
*/
setNavBarStyle() {
if (this.type !== 'line' || !this.$refs.tabkey) return {};
const tabKey = this.curActive;
const elem = this.$refs.tabkey[tabKey];
const offsetWidth = `${elem.offsetWidth || 0}px`;
const offsetLeft = `${elem.offsetLeft || 0}px`;
this.navBarStyle = {
width: offsetWidth,
transform: `translate3d(${offsetLeft}, 0, 0)`,
transitionDuration: `${this.duration}s`
};
},
handleTabClick(index) {
if (this.tabs[index].disabled) {
this.$emit('disabled', index);
return;
}
correctActive(active) {
active = +active;
const exist = this.tabs.some(tab => tab.index === active);
this.curActive = exist ? active : (this.tabs[0].index || 0);
},
onClick(index) {
if (this.tabs[index].disabled) {
this.$emit('disabled', index);
} else {
this.$emit('click', index);
this.curActive = index;
},
/**
* 将当前value值转换为需要translate的值
*/
value2Translate(value) {
/* istanbul ignore if */
if (!this.$refs.tabkey) return 0;
const tab = this.$refs.tabkey[value];
const maxTranslate = this.maxTranslate;
const tabWidth = tab.offsetWidth;
const tabOffsetLeft = tab.offsetLeft;
let translate = tabOffsetLeft + (tabWidth * 2.7) - this.swipeWidth;
if (translate < 0) {
translate = 0;
}
return -1 * (translate > maxTranslate ? maxTranslate : translate);
},
initEvents() {
const el = this.$refs.swipe;
if (!el || this.isInitEvents) return;
this.isInitEvents = true;
let swipeState = {};
swipe(el, {
start: event => {
swipeState = {
start: new Date(),
startLeft: event.pageX,
startTranslateLeft: translateUtil.getElementTranslate(el).left
};
},
drag: event => {
this.isSwiping = true;
swipeState.left = event.pageX;
const deltaX = swipeState.left - swipeState.startLeft;
const translate = swipeState.startTranslateLeft + deltaX;
/* istanbul ignore else */
if (translate > 0 || (translate * -1) > this.maxTranslate) return;
translateUtil.translateElement(el, translate, null);
},
end: () => {
this.isSwiping = false;
}
});
},
doOnValueChange() {
const value = +this.curActive;
const swipe = this.$refs.swipe;
translateUtil.translateElement(swipe, this.value2Translate(value), null);
}
},
scrollIntoView() {
if (!this.scrollable) {
return;
}
const tab = this.$refs.tabs[this.curActive];
const { nav } = this.$refs;
const { winWidth } = this;
const { scrollLeft } = nav;
const { offsetLeft, offsetWidth: tabWidth } = tab;
// out of right side
if ((winWidth + scrollLeft - offsetLeft - tabWidth * 1.8) < 0) {
this.scrollTo(nav, scrollLeft, offsetLeft + tabWidth * 1.8 - winWidth);
}
// out of left side
else if (offsetLeft < (scrollLeft + tabWidth * 0.8)) {
this.scrollTo(nav, scrollLeft, offsetLeft - tabWidth * 0.8);
}
},
scrollTo(el, from, to) {
let count = 0;
const frames = Math.round(this.duration * 1000 / 16);
const animate = () => {
el.scrollLeft += (to - from) / frames;
if (++count < frames) {
raf(animate);
}
}
animate();
}
};
}
};
</script>
-46
View File
@@ -1,46 +0,0 @@
import Vue from 'vue';
let isSwiping = false;
const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window;
export default function(element, options) {
const moveFn = function(event) {
if (options.drag) {
options.drag(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
};
const endFn = function(event) {
if (!supportTouch) {
document.removeEventListener('mousemove', moveFn);
document.removeEventListener('mouseup', endFn);
}
isSwiping = false;
if (options.end) {
options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
};
element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) {
if (isSwiping) return;
if (!supportTouch) {
document.addEventListener('mousemove', moveFn);
document.addEventListener('mouseup', endFn);
}
isSwiping = true;
if (options.start) {
options.start(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
});
if (supportTouch) {
element.addEventListener('touchmove', moveFn);
element.addEventListener('touchend', endFn);
element.addEventListener('touchcancel', endFn);
}
};