實現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會警告你:warning:,當然你沒用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', }, ]} />