起因:产品给了一个需求,要求写一个跟微信朋友圈一样,取首字母产生的通讯录标签页,
因为很多人要求源码给上
现在贴上源码
源码地址
写了一遍,发现意外的好写
最终效果
数据采用了随机字符串的方法,并且使用数组方法排序,
// 去除数字key
let regExp = new RegExp(/^[0-9]$/);
let data = [];
let title = [];
for (let i = 0; i < 200; i++) {
let s = Math.random().toString(36).substr(2);
let subStr = s.substr(0, 1);
if (regExp.test(subStr)) {
i--;
continue;
}
if (data.indexOf(subStr) === -1) {
data.push(subStr);
}
data.push(s);
let number = title.indexOf(subStr);
if (number === -1) {
title.push(s.substr(0, 1));
}
}
data.sort();
title.sort();
核心方法就是根据找到其中的索引,跳转位置,通过window.scrollTo,进行跳转,用findIndex来进行查找准确标签位置,进行跳转,
主要计算公式:
需要跳转的高度 = ( 条目高度 * 索引位置 [+ 其他控件的高度,在其之上的控件])
注: 每个条目大小必须已知
{
this.state.title.map((item, position) => {
return <div
className={item === this.state.currentTitle ? "select-title" : ""}
onClick={() => {
let find = this.state.data.findIndex(obj => obj === item);
window.scrollTo(0, find * 100 - position * 100 + position * 48 + 358);
}}>{item.toLocaleUpperCase()}</div>
})
}
添加滚动监听器
componentWillUnmount() {
window.removeEventListener('scroll', this.onScrollChange, false)
}
componentDidMount() {
window.addEventListener('scroll', this.onScrollChange, false)
}
滚动计算方式
首先获取滚动距离 ,其次先定义一个起始位置,然后获取位置距离,由于字母key和内容item的高度不一致,所以需要获取到两者之间的item,所以获取的是字母key和下一个字母key的item数量.由length记录,去记录item的高度.其次获取到key的数量*key的高度.如果滚动距离小于此高度,则说明,目前在的位置是当前key.
注: item和key的高度需要已知
onScrollChange = (e) => {
// console.log();
let scroll = window.scrollY [- 顶部控件高度];
let current = "a";
let {data, title} = this.state;
let length = 0;
for (let i = 0; i < title.length; i++) {
let currentTitle = title[i];
if (i === title.length - 1) {
current = currentTitle;
break;
}
let nextTitle = title[i + 1];
let currentIndex = data.indexOf(currentTitle);
let nextIndex = data.indexOf(nextTitle);
let sum = nextIndex - (currentIndex + 1);
length = length + sum * 100;
let number = length + (i + 1) * 48;
if (scroll < number) {
current = currentTitle;
break;
}
}
this.setState({currentTitle: current})
};
如果导致滚动监听不到的情况,
一般为overflow:auto之类的情况,如果父容器设置了,可能导致不成功
源码,仅供参考
import React from 'react'
import BaseComponent from "../../../../page/base/BaseComponent";
import Grid from "../grid/Grid";
import {connect} from "react-redux";
import {withRouter} from "react-router";
import * as PATH from "../../../../conts/path";
const regExp = new RegExp(/^[0-9]\d*\w*$/);
class BrandItem1 extends BaseComponent {
constructor(props) {
super(props);
this.state = {
data: [],
title: [],
header: "brand",
currentTitle: "a"
}
}
onScrollChange = (e) => {
let scroll = window.scrollY - 358;
let current = this.state.title[0];
let {data, title} = this.state;
let length = 0;
for (let i = 0; i < title.length; i++) {
let currentTitle = title[i];
if (i === title.length - 1) {
current = currentTitle;
break;
}
let nextTitle = title[i + 1];
let currentIndex = data.indexOf(currentTitle);
let nextIndex = data.indexOf(nextTitle);
let sum = nextIndex - (currentIndex + 1);
length = length + sum * 100;
let number = length + (i + 1) * 48;
if (scroll < number) {
current = currentTitle;
break;
}
}
this.setState({currentTitle: current})
};
componentDidMount() {
window.addEventListener('scroll', this.onScrollChange);
}
componentWillMount() {
this.setData(this.props)
}
componentWillReceiveProps(nextProps) {
this.setData(nextProps);
}
setData(props) {
const {brandSource} = props;
let data = [];
let title = [];
let data2 = [];
brandSource.forEach(item => {
const s = item.brandName;
let subStr = s.substr(0, 1);
if (regExp.test(subStr)) {
// 匹配上数字开头
data2.push(s);
} else {
if (data.indexOf(subStr) === -1) {
//传入标题
data.push(subStr);
}
data.push(s);
let number = title.indexOf(subStr);
if (number === -1) {
title.push(s.substr(0, 1));
}
}
});
data.sort();
title.sort();
if (data2.length !== 0) {
data.push("#");
data2.forEach((item, index) => {
data.push(item)
});
title.push("#");
}
this.setState({title, data}, () => {
this.onScrollChange(null);
});
}
componentWillUnmount() {
window.removeEventListener('scroll', this.onScrollChange)
}
render() {
const {title, data} = this.state;
let innerHeight = window.innerHeight;
let top = innerHeight * 0.1;
let number = innerHeight * 0.8 / title.length;
return <React.Fragment>
<div className={"item1"}>
<div>
<Grid dataSource={this.props.dataSource} itemStyle={{margin: "0 19px 0"}}/>
</div>
<div>
{
data.map((item, position) => {
let find = title.find(obj => item === obj);
if (find) {
return <div className={"padding-left-30 list-item-1 kalinga-font"}
>{item.toLocaleUpperCase()}</div>
}
return <div className={"padding-left-30 list-item-2 kalinga-font"} onClick={(e) => {
const brand = this.props.brandSource.find(n => n.brandName === item);
this.startPage(PATH.BRAND +"?id=" + brand.id, false);
}}
>{item}</div>
})
}
</div>
<div className={"title"} style={{top: top, height: innerHeight * 0.8}}>
{
title.map((item, position) => {
return <div
style={{height: number, lineHeight: number + "px"}}
className={item === this.state.currentTitle ? "select-title" : ""}
onClick={() => {
let find = data.findIndex(obj => obj === item);
setTimeout(() => {
window.scrollTo(0, find * 100 - position * 100 + position * 48 + 358);
}, 10);
}}>
<div
className={item === this.state.currentTitle ? "select-title-span" : ""}>{item.toLocaleUpperCase()}</div>
</div>
})
}
</div>
</div>
</React.Fragment>
}
}
const mapStateToProps = state => {
const {pageInfo, historyPage} = state;
return {pageInfo, historyPage};
};
export default withRouter(connect(mapStateToProps)(BrandItem1));