最近坐公交发现微信已经有很多小程序支持实时公交了,可以随时查看当前站点最近车辆位置,很是方便。出于前端程序员的本能,遇到一些自己没做过的东西就想学学咋做的。
那么,废话不多说,开始搞起。
首先,先看一下这个公交站点列表,这个列表支持滚动,支持点击站点切换,站点左右滑动隐藏后还会出现悬浮按钮,点击可以回到当前站点。
那么,就可以一步步得往下走了:
1、支持滚动,这个可以直接使用微信小程序的scroll-view组件,没什么问题;
2、支持站点切换,其实不就是我们平时常用的step步骤条吗,比如element的el-steps组件。那这边前端的UI设计就可以完全参考element-ui的step组件实现:
于是我就打开了element-ui的官方文档,打开F12,看一下组件设计,外层是一个flex盒子,里面每个子item(每一步)的flex-shrink:1 最后一个item的flex-shrink:0这样就保证了从起始到终点刚好连成一条线。只不过最后的item没有那条线了。
这个设计一般是针对父容器盒子宽度固定,子item平分的设计。
但是在移动端,站点列表那么多,总宽度肯定不是固定的,但是每条item的宽度是固定的,这样的话在移动端就可以让每个子item的flex-shrink:取固定值就可以了。
<scroll-view class="line-scroll" scroll-x="{{ true }}" scroll-y="{{false}}" scroll-with-animation="{{ true }}">
<view class="line-list">
<view class="line-item {{ activeIndex===index?'active':'' }}" id="{{ 'activeItem'+item.stationId }}" wx:for="{{list}}" wx:key="stationId" data-id="{{ item.stationId }}" catch:tap="switchTab">
<view class="station-top-icon" wx:if="{{ item.isArrived }}"></view>
<view class="station-center-icon" wx:if="{{ item.isPast }}"></view>
<view class="step-icon-container">
<view class="icon-circle"></view>
<!-- <view class="icon-circle"></view> -->
</view>
<view class="step-line"></view>
<view class="step-title">{{ item.stationName }}</view>
</view>
</view>
</scroll-view>
.line-scroll{
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow-x: auto;
.line-list{
position: relative;
height: 100%;
display: flex;
padding: 10rpx 40rpx 0;
.line-item{
position: relative;
flex: 0 0 150rpx;
.step-line{
position: absolute;
top: 70rpx;
left: 0;
width: 100%;
height: 10rpx;
background-color: #0CC777;
&::after{
position: absolute;
display: block;
content: '';
z-index: 1;
left: 0%;
top: 50%;
width: 6rpx;
height: 10rpx;
background: url('https://file.40017.cn/groundtraffic/urpt/bus-path.png') center center no-repeat;
background-size: 100% 100%;
transform: translateX(-50%) translateY(-50%);
animation: infinitescroll 1.5s linear infinite;
}
}
.step-icon-container{
position: absolute;
left: -15rpx;
top: 75rpx;
z-index: 1;
padding: 0 5rpx;
transform: translateY(-50%);
.icon-circle{
box-sizing: border-box;
width: 26rpx;
height: 26rpx;
border: 4rpx solid #0CC777;
border-radius: 50%;
background-color: #fff;
}
}
&.active{
.step-icon-container{
position: absolute;
left: -27rpx;
top: 75rpx;
z-index: 1;
padding: 0 5rpx;
transform: translateY(-50%);
.icon-circle{
box-sizing: border-box;
width: 50rpx;
height: 50rpx;
border: none;
border-radius: 50%;
background:url('https://file.40017.cn/groundtraffic/urpt/station-icon.png') center center no-repeat;
background-size: 100% 100%;
}
}
.step-title{
color: #00C777;
font-weight: bold;
}
}
.step-title{
position: absolute;
top: 100rpx;
left: -20rpx;
width: 40rpx;
height: 310rpx;
font-size: 30rpx;
writing-mode: vertical-lr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.station-top-icon{
position: absolute;
top: 20rpx;
left: -20rpx;
width: 48rpx;
height: 39rpx;
background: url('https://file.40017.cn/groundtraffic/urpt/bus-icon.png') center center no-repeat;
background-size: 100% 100%;
}
.station-center-icon{
position: absolute;
top: 20rpx;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 39rpx;
background: url('https://file.40017.cn/groundtraffic/urpt/bus-icon.png') center center no-repeat;
background-size: 100% 100%;
}
}
.line-item:last-child{
// position: absolute;
flex: 0 0 1rpx;
right: 1rpx;
width: auto;
.step-line{
width: 0;
}
}
}
}
于是基本的ui就有了
第二步、考虑如何实现点击每个站点,能够自动滚动到相应位置。
这个就需要了解scroll-view这个组件了,它有一个scroll-left属性,绑定的滚动值。我们只需要点击相应站点,计算出相应的滚动的scrollLeft值,然后赋值给它就可以了。这个好办:
<scroll-view class="line-scroll" scroll-x="{{ true }}" scroll-y="{{false}}" scroll-with-animation="{{ true }}" scroll-left="{{ scrollLeft }}" bindscroll="handleScroll">
<view class="line-list">
<view class="line-item {{ activeIndex===index?'active':'' }}" id="{{ 'activeItem'+item.stationId }}" w<!-- -->x:for="{{list}}" wx:key="stationId" data-id="{{ item.stationId }}" catch:tap="switchTab">
<view class="station-top-icon" wx:if="{{ item.isArrived }}"></view>
<view class="station-center-icon" wx:if="{{ item.isPast }}"></view>
<view class="step-icon-container">
<view class="icon-circle"></view>
<!-- <view class="icon-circle"></view> -->
</view>
<view class="step-line"></view>
<view class="step-title">{{ item.stationName }}</view>
</view>
</view>
</scroll-view>
我们绑定scroll-left,然后绑定站点点击事件switchTab,还有绑定scroll-view的bindscroll滚动监听事件。
switchTab(e) {
let { activeIndex } = this.data
let { id } = e.currentTarget.dataset
let { list } = this.properties
let currentActiveIndex = list.findIndex(item => item.stationId === id)
this.triggerEvent('inform', {
type: 'switch',
id
})
this.setData({
currentActiveIndex
})
}
我这里其实是做成了一个组件,这边是向父组件通知切换当前激活的站点下标。其实原理就是算出当前点击的站点的下标。然后改变activeIndex,并且监听每个站点下标的变化,一旦变化,执行setScrollLeft事件:
Component({
properties: {
list: {
type: Array,
value: []
},
activeIndex: {
type: Number,
value: 0
},
activeStation: {
type: String,
value: ''
},
rr: {
type: Number,
value: 2
}
},
observers: {
activeIndex(val) {
this.setScrollLeft(val)
}
},
methods:{
// 设置滚动值
setScrollLeft(val){
let { rr } = this.properties
this.setData({
scrollLeft: val < 4 ? 0 : (Math.abs((val - 3)) * 150 / rr)
})
},
}
})
这里要说明下,这个rr是rpx与px直接转换的倍率。这里将rpx转换成px赋值给scroll-left。 这里为什么有val < 4 ? 0 : (Math.abs((val - 3)) * 150 / rr)这个计算公式,是因为我的页面设计站点在可视范围内最多就5个,150是每个站点之间的距离(rpx)。所以下标小于4的站点,它的滚动值其实就是0,没有滚动。
这样就实现了组件列表来回点击切换,自动滚动当相应站点的功能了。
第三步,如何实现当前站点左右滚动隐藏后显示滚动条。
其实就是监听handscroll事件,计算当前激活的站点的scrollLeft值是否在可视范围内。
wxml
<view class="bus-line">
<scroll-view class="line-scroll" scroll-x="{{ true }}" scroll-y="{{false}}" scroll-with-animation="{{ true }}" scroll-left="{{ scrollLeft }}" bindscroll="handleScroll">
<view class="line-list">
<view class="line-item {{ activeIndex===index?'active':'' }}" id="{{ 'activeItem'+item.stationId }}" w<!-- -->x:for="{{list}}" wx:key="stationId" data-id="{{ item.stationId }}" catch:tap="switchTab">
<view class="station-top-icon" wx:if="{{ item.isArrived }}"></view>
<view class="station-center-icon" wx:if="{{ item.isPast }}"></view>
<view class="step-icon-container">
<view class="icon-circle"></view>
<!-- <view class="icon-circle"></view> -->
</view>
<view class="step-line"></view>
<view class="step-title">{{ item.stationName }}</view>
</view>
</view>
</scroll-view>
<view class="float-left" wx:if="{{ showLeft }}" catch:tap="goActiveStation">{{ activeStation}}</view>
<view class="float-right" wx:if="{{ showRight }}" catch:tap="goActiveStation">{{ activeStation }}</view>
</view>
对应的css
.float-left{
position: absolute;
background: #fff;
top: 105rpx;
left: 8rpx;
width: 64rpx;
line-height: 64rpx;
padding: 35rpx 0 10rpx;
border-radius: 8rpx;
font-size: 30rpx;
writing-mode: vertical-lr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 2px 4px 14px 0px rgba(0, 0, 0, 0.2);
&::after{
position: absolute;
content: '';
width: 16rpx;
height: 12rpx;
background: url('https://file.40017.cn/groundtraffic/urpt/to_left.png') center center no-repeat;
background-size: 100%;
top: 10rpx;
left: 24rpx;
}
}
.float-right{
position: absolute;
background: #fff;
top: 105rpx;
right: 8rpx;
width: 64rpx;
line-height: 64rpx;
padding: 35rpx 0 10rpx;
border-radius: 8rpx;
font-size: 30rpx;
writing-mode: vertical-lr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 2px 4px 14px 0px rgba(0, 0, 0, 0.2);
&::after{
position: absolute;
content: '';
width: 16rpx;
height: 12rpx;
background: url('https://file.40017.cn/groundtraffic/urpt/to_right.png') center center no-repeat;
background-size: 100%;
top: 10rpx;
left: 24rpx;
}
}
对应的js
methods:{
handleScroll(e) {
let { scrollLeft } = e.detail
this.setScrollLeftVal(scrollLeft)
this.checkVisible(scrollLeft)
},
// 保存滚动值
setScrollLeftVal:debounce(function(val){
this.setData({
scrollLeftVal: val
})
},500),
// 检查是否需要隐藏左右浮动站点
checkVisible(scrollLeft){
let { rr } = this.properties
let leftHideDis = (this.data.activeIndex * 150 + 40 + 23) / rr
let rightHideDis = (this.data.activeIndex * 150 + 40 - 23)
if (leftHideDis < scrollLeft) {
this.setData({
showLeft: true
})
} else if (rightHideDis > 750 && rightHideDis - scrollLeft * rr > 750) {
this.setData({
showRight: true
})
} else {
this.setData({
showLeft: false,
showRight: false
})
}
},
goActiveStation(){
let { activeIndex } = this.properties
this.setScrollLeft(activeIndex)
},
}
这里前端wxml添加左右浮动的按钮,绝对定位。这里面40是起始站点与终点战距离两边的padding值、23是每个站点的图标的半径。其实就是为了计算刚好隐藏当前站点的scrollLeft边界值。然后切换显示与隐藏。 这样基本上一个站点列表的左右滑动、切换,激活站点的悬浮隐藏功能就都有了。剩下的就是对接接口数据切换相应的map组件轨迹、标记点等等了。