实现tab标签下选中条滑动效果-react组件
这个是模仿ant design的Tabs控件,当切换tab时,下面的蓝色条滑过的效果。
点击查看效果
我只是封装了tab的头部标签,并没有包含内容部分。
我的最终结果
相关技术
transform
transform
属性允许你旋转,缩放,倾斜或平移给定元素。这是通过修改CSS视觉格式化模型的坐标空间来实现的。
通过看ant design的代码,他使用的是translate3d
平移函数。
transform: translate3d(100px, 0px, 0px)
translate3d函数
translate3d() 这个CSS 函数用于移动元素在3D空间中的位置。 这种变换的特点是三维矢量的坐标定义了它在每个方向上的移动量。
语法
translate3d(tx, ty, tz)
transition
transition
控制滑动速度及滑动时间等。不用这个属性,效果没那么自然。
transition
CSS属性是一个简写属性,用于transition-property
,transition-duration
,transition-timing-function
和transition-delay
。上面就是实现需要用到的比较不常见的技术,所以专门列举出来。
布局分析
- 首先一个外层的div当做容器
headerContainer
- 里面分为上下两部分,上面就是包含各个“标签”的容器
header
,下面是滑动条tab_bar
注意:滑动条是一个专门的div来实现,并不是“标签”容器的下边框
- 标签容器里面放各个“标签”元素
代码布局如下:
<div class="headerContainer"> <div class="header"> <div class="headItem">tab 1</div> <div class="headItemChecked">tab 2</div> </div> <div class="tab_bar"></div> </div>
说明headerContainer
目前没有需要的css,由于我是用less写的,只是用它当做一个容器来用。header
和headItemChecked
设置标签的排列方式、字体等样式tab_bar
的css样式是关键,它设置选中条
的样式,值得注意的是,它需要和标签的状态和宽度保持一致。
如何和标签状态保持一致
当选中一个标签时,“选中条”需要滑动到对应的标签下面。
这个通过设置“选中条”的平移位置来实现,这个可以设置translate3d
的参数来实现
translate3d(${checkedPosition}px, 0px, 0px)
当点击标签时,动态设置checkedPosition
的值即可。
onClickHeader = (checkedHead, index) => { this.setState({ checkedHead, checkedPosition: index * 100 }); };
但是这时虽然能滑过去,但是没有那种平滑的滑动效果,实现这个效果就需要transition
来实现。
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), -webkit-transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
这个和ant design的参数保持一致
但是每个标签的内容导致宽度是不一样的,所以不能乘以换一个固定的值(100)来计算每次平移的位置,需要每个标签的实际宽度来决定平移的位置。
计算每个标签的宽度
这个就用到了react
不推荐使用的ref
属性了。这里推荐使用回调函数的方式,不然eslint会警告你⚠️,当然你没用eslint就无所谓了。
通过ref来获取元素的宽度,然后计算容器的宽度
、选中条的宽度和位置
。
<div ref={r => { this[`ref_${index}`] = r; }} key={item.code} ....省略部分 //计算宽度 onClickHeader = (checkedHead, index) => { const preWidth = index > 0 ? this[`ref_${index - 1}`].offsetWidth : 0; const barWidth = this[`ref_${index}`].offsetWidth; this.setState({ checkedHead, checkedPosition: index * preWidth, barWidth }); };
解决offsetWidth四舍五入的问题
offsetWidth虽然能获取元素的宽度,但是在使用过程中发现,它返回的都是整数,进行了四舍五入
的情况,当宽度遇到小于0.5的情况,就会引起内容换行
了,很不美观,所以不能使用offsetWidth.
解决方法如下:
- Element.getBoundingClientRect()
Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。
this[`ref_${index}`].getBoundingClientRect().width;//192.243
返回的是包含小数的数字,比如192.243
- Window.getComputedStyle()
Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。 私有的CSS属性值可以通过对象提供的API或通过简单地使用CSS属性名称进行索引来访问。
getComputedStyle(this[`ref_${index}`], null).getPropertyValue('width');//192.243px
返回的是带单位(px)的值,比如192.243px。
由于涉及到计算,我上面使用了第一种解决方法。
完整code
index.js
import React, { PureComponent } from 'react'; import styles from './index.less'; export default class TabHeader extends PureComponent { constructor(props) { super(props); const { defaultHead } = this.props; this.state = { containerWidth: 1500, checkedPosition: 0, barWidth: 70, checkedHead: defaultHead, }; } componentDidMount() { const { heardList } = this.props; let containerWidth = 0; (heardList || []).forEach((item, index) => { containerWidth += this[`ref_${index}`].getBoundingClientRect().width; }); this.setState({ barWidth: this.ref_0.getBoundingClientRect().width, containerWidth }); } onClickHeader = (checkedHead, index) => { let preWidth = 0; for (let i = 0; i < index; i += 1) { preWidth += this[`ref_${i}`].offsetWidth; } const barWidth = this[`ref_${index}`].offsetWidth; this.setState({ checkedHead, checkedPosition: preWidth, barWidth }); }; render() { const { checkedHead, checkedPosition, containerWidth, barWidth } = this.state; const { heardList, source } = this.props; return ( <div className={styles.container} style={{ width: `${containerWidth}px` }}> <div className={styles.headerContainer}> <div className={styles.header}> {heardList.map((item, index) => ( <div ref={r => { this[`ref_${index}`] = r; }} key={item.code} className={checkedHead === item.code ? styles.headItemChecked : styles.headItem} onClick={this.onClickHeader.bind(this, item.code, index)} > {item.text} {item.num} </div> ))} </div> <div className={styles.tab_bar} style={{ transform: `translate3d(${checkedPosition}px, 0px, 0px)`, width: `${barWidth}px`, }} /> </div> <div className={styles.list}> {source.map(item => ( <div key={item.id} className={styles.row}> <div> <div className={styles.name}>{item.name}</div> <div className={styles.phone}>{item.phone}</div> </div> <div className={styles.count}> <span className={styles.doing}>{item.doing}</span> / <span className={styles.error}> {item.error}</span> / <span className={styles.all}> {item.all}</span> </div> </div> ))} </div> </div> ); } }
index.less
.container { background: rgba(255, 255, 255, 1); box-sizing: border-box; .headerContainer { position: relative; box-sizing: border-box; .header { display: flex; box-sizing: border-box; .headItem { box-sizing: border-box; font-size: 14px; font-family: PingFangSC-Regular; font-weight: 400; color: rgba(51, 51, 51, 1); text-align: center; padding: 12px 17px; cursor: pointer; border-bottom: 4px solid rgba(232, 232, 232, 1); transition: border 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } .headItemChecked { .headItem; color: rgba(0, 155, 255, 1); } } .tab_bar { position: absolute; bottom: 0px; box-sizing: border-box; background-color: #1890ff; height: 4px; transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), -webkit-transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } } .list { .row { display: flex; justify-content: space-between; padding: 14px 15px 10px 8px; font-size: 12px; font-family: PingFangSC-Regular; font-weight: 400; color: rgba(102, 102, 102, 1); border-bottom: 1px solid rgba(232, 232, 232, 1); .name { font-size: 14px; font-weight: 600; color: rgba(51, 51, 51, 1); } .phone { margin-top: 15px; } .count { font-size: 14px; font-family: PingFangSC-Semibold; font-weight: 600; .doing { color: rgba(24, 137, 250, 1); } .error { color: #EB9E08; } .all { color: #5f636b; } } } } }
调用demo
<TabHeader defaultHead="abc" heardList={[ { code: 'abc', text: '较长的名字数量', num: '10' }, { code: 'abcd', text: '男人', num: '101' }, { code: 'abce', text: '美女数', num: '121' }, ]} source={[ { id: '12121', name: '刘医生', phone: '16807656551', doing: '10', error: '212', all: '32', }, { id: '1211', name: '张无忌', phone: '16807656551', doing: '10', error: '22', all: '322', }, ]} />