很久沒有更新博客了,原因總有許多。但是每每都會回來看看有沒有信息,看到寫文章中還有不少只寫了開頭未發布的文章,心中又有不舍。這是關于streetscape.gl的第三篇筆記,或許后面還會有更新,亦或許沒有更新換做其他領域的文章。不管怎么說,把streetscape.gl的一個閉環講完也算是了卻自己心頭一件事。
這篇主要講進度條,在各類看板中進度條是必備的。播放控制、滑塊定位、刻度展示等等。當然我們也可以以此為模板進行擴展,例如:添加倍速、標記關鍵時間節點、添加前至進度條等等。在對歷史數據回放時,汽車工程師和進度條之間的交互就愈發頻繁,所以進度條準確、便利給客戶帶來的感受提升不亞于在地圖頁面。
streetscape.gl demo上的進度條由兩部分組成:一個我稱之為主進度條、另一個我管它叫前至進度條(舉個例子:當前主進度條播放到10s處,如果前至進度條設置為2s,則在LogViewer中不僅能看到10s處的數據也可以看到12s處的數據。為什么有這樣的功能,可能只有客戶知道吧)。當然streetscape.gl是通過組合的方式提供的一個組件。
如何使用
在streetscape.gl提供的get-started我們可以看到它是這樣被加入進來的。
import {
LogViewer,
PlaybackControl,
StreamSettingsPanel,
MeterWidget,
TrafficLightWidget,
TurnSignalWidget,
XVIZPanel,
VIEW_MODE
} from 'streetscape.gl';
render() {
const {log, settings} = this.state;
return (
...
<div id="timeline">
<PlaybackControl
width="100%"
log={log}
formatTimestamp={x => new Date(x * TIMEFORMAT_SCALE).toUTCString()}
/>
</div>
...
);
}
可以看到使用方便,只要輸入相關屬性即可。
如何實現
在./modules/core/src/components/playback-control中,我們可以看到這個組件的定義。其中index.js文件定義的PlaybackControl組件,我們可以視其為容器組件,主要負責組件的邏輯部分;而dual-playback-control.js文件定義的DualPlaybackControl組件,其組合了streetscape.gl/monochrome中的PlaybackControl組件,我們可以視其為UI組件,主要負責頁面渲染部分。
先看index.js中PlaybackControl組件
class PlaybackControl extends PureComponent {
...
}
這里插入一下PureComponent和Component之間的區別。
得益于作者這兩天看的《React狀態管理及同構實戰》這本書,對React中的部分細節有所感悟,邊看該框架的同時,也看看它運用React的最佳實踐,精華部分當然應該吸收借鑒。
一句話概括就是:PureComponent通過props和state的淺對比來實現 shouldComponentUpate(),而Component沒有,而shouldComponentUpdate是決定界面是否需要更新的鑰匙。
再看其屬性的定義:
static propTypes = {
// from log
timestamp: PropTypes.number,
lookAhead: PropTypes.number,
startTime: PropTypes.number,
endTime: PropTypes.number,
buffered: PropTypes.array,
// state override
isPlaying: PropTypes.bool,
// config
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
style: PropTypes.object,
compact: PropTypes.bool,
className: PropTypes.string,
step: PropTypes.number,
padding: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
tickSpacing: PropTypes.number,
markers: PropTypes.arrayOf(PropTypes.object),
formatTick: PropTypes.func,
formatTimestamp: PropTypes.func,
// dual playback control config
maxLookAhead: PropTypes.number,
formatLookAhead: PropTypes.func,
// callbacks
onPlay: PropTypes.func,
onPause: PropTypes.func,
onSeek: PropTypes.func,
onLookAheadChange: PropTypes.func
};
從屬性的定義上,我們可以看到,有部分屬性來自log以獲得這批數據的起止時間、時間間隔、前至多久等,還有當前播放狀態,以及在頁面上的樣式用以顯示。
我們從React幾個狀態函數入手看看,組件各個階段都做了什么。先存一張圖,react組件各狀態流程(不能爛熟于心只能一一對照著看了)
先看看componentWillReceiveProps
componentWillReceiveProps(nextProps) {
if ('isPlaying' in nextProps) {
this.setState({isPlaying: Boolean(nextProps.isPlaying)});
}
}
當組件運行時接到新屬性,根據新屬性更新播放狀態。這個相對單一,再看看componentDidUpdate
componentDidUpdate(prevProps, prevState) {
const {isPlaying} = this.state;
if (isPlaying && prevState.isPlaying !== isPlaying) {
this._lastAnimationUpdate = Date.now();
this._animationFrame = window.requestAnimationFrame(this._animate);
}
}
當狀態或者屬性發生改變,引起組件的重新渲染并觸發。
當播放狀態為true同時上一播放狀態為false時,讓瀏覽器在下一次重繪之前執行_animate動畫,同時它返回一個long值_animationFrame,是回調列表中唯一的標識。是個非零值,沒別的意義。你可以傳這個值給 window.cancelAnimationFrame()
以取消回調函數。
再看看_animate這個動畫
_animate = () => {
if (this.state.isPlaying) {
const now = Date.now();
const {startTime, endTime, buffered, timestamp} = this.props;
const {timeScale} = this.state;
const lastUpdate = this._lastAnimationUpdate;
const {PLAYBACK_FRAME_RATE, TIME_WINDOW} = getXVIZConfig();
// avoid skipping frames - cap delta at resolution
let timeDeltaMs = lastUpdate > 0 ? now - lastUpdate : 0;
timeDeltaMs = Math.min(timeDeltaMs, 1000 / PLAYBACK_FRAME_RATE);
let newTimestamp = timestamp + timeDeltaMs * timeScale;
if (newTimestamp > endTime) {
newTimestamp = startTime;
}
// check buffer availability
if (buffered.some(r => newTimestamp >= r[0] && newTimestamp <= r[1] + TIME_WINDOW)) {
// only move forward if buffer is loaded
// otherwise pause and wait
this._onTimeChange(newTimestamp);
}
this._lastAnimationUpdate = now;
this._animationFrame = window.requestAnimationFrame(this._animate);
}
};
當播放狀態為true時,開始執行該動畫。
先計算當前時間和最后一次更新時間的時間差timeDeltaMs。
通過時間差timeDeltaMs和屬性中的該log對應的timestamp求和獲得新的newTimestamp。
檢查buffered中是否有區間正好卡住newTimestamp,如果有則向前移動否則就暫停并等待。(這段估計是為了控制進度條中的slider)。
設置最近的動畫更新時間點為now。
為了在瀏覽器下次重繪之前繼續更新下一幀動畫,那么回調函數_animate中自身必須再次調用window.requestAnimationFrame()。
這里調用了一個_onTimeChange函數
_onTimeChange = timestamp => {
const {log, onSeek} = this.props;
if (!onSeek(timestamp) && log) {
log.seek(timestamp);
}
};
該函數主要是讓log數據seek到指定的timestamp。這里的log是 XVIZStreamLoader或XVIZLiveLoader或 XVIZFileLoader中的一種,其都繼承于XVIZLoaderInterface。這部分計劃再單開一片筆記講解一下。
再看看componentWillUnmount,這一階段主要將注冊的動畫取消掉。
componentWillUnmount() {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
}
}
該組件的主要生命周期函數介紹完后,再看看render函數。
render() {
const {startTime, endTime, timestamp, lookAhead, buffered, ...otherProps} = this.props;
if (!Number.isFinite(timestamp) || !Number.isFinite(startTime)) {
return null;
}
const bufferRange = buffered.map(r => ({
startTime: Math.max(r[0], startTime),
endTime: Math.min(r[1], endTime)
}));
return (
<DualPlaybackControl
{...otherProps}
bufferRange={bufferRange}
currentTime={timestamp}
lookAhead={lookAhead}
startTime={startTime}
endTime={endTime}
isPlaying={this.state.isPlaying}
formatTick={this._formatTick}
formatTimestamp={this._formatTimestamp}
formatLookAhead={this._formatLookAhead}
onSeek={this._onSeek}
onPlay={this._onPlay}
onPause={this._onPause}
onLookAheadChange={this._onLookAheadChange}
/>
);
}
}
該處功能也很明確,首先是判斷timestamp和startTime是否為有窮數,如若不是直接返回為null。該部分有可能是針對Live數據源,對于直接是實時的數據源就不用安排上進度條了。然后將屬性、事件,通過props向子組件傳遞,其中對bufferRange屬性值和全局的startTime、endTime進行比較,使其落入到最小范圍區間內。
接著又定義了一個getLogState的函數,主要是從log數據體中獲得timestamp、lookAhead、startTime等數據。
const getLogState = log => ({
timestamp: log.getCurrentTime(),
lookAhead: log.getLookAhead(),
startTime: log.getLogStartTime(),
endTime: log.getLogEndTime(),
buffered: log.getBufferedTimeRanges()
});
這個函數是用于connectToLog這個高階組件包裝函數中的。
export default connectToLog({getLogState, Component: PlaybackControl});
這里,作者把所有需要綁定log相應屬性生成高階組件,將其抽出成一個高階組件生成函數——connectToLog,你可以在源碼的很多地方瞧見它。是一個很不錯的實踐,可供參考。
這部分先講到這,再看看DualPlaybackControl這個UI組件,在dual-playback-control.js文件中。
const LookAheadContainer = styled.div(props => ({
display: 'flex',
alignItems: 'center',
width: 200,
'>div': {
flexGrow: 1
},
...evaluateStyle(props.userStyle, props)
}));
const LookAheadTimestamp = styled.span(props => ({
marginLeft: props.theme.spacingNormal,
marginRight: props.theme.spacingNormal,
...evaluateStyle(props.userStyle, props)
}));
先是定義了兩個dom的容器,一個用以存放前至進度條,另一個用以存放前至多久數值。
const lookAheadMarkerStyle = props => ({
position: 'absolute',
boxSizing: 'content-box',
borderStyle: 'solid',
marginTop: 6,
marginLeft: -6,
borderWidth: 6,
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderTopColor: '#888',
borderBottomStyle: 'none',
transitionProperty: 'left',
transitionDuration: props.isPlaying ? '0s' : props.theme.transitionDuration,
...evaluateStyle(props.userStyle, props)
});
這部分用以定義主進度條前至滑塊的樣式。其中evaluateStyle(在modules\monochrome\src\shared\theme.js文件中)函數定義如下
export function evaluateStyle(userStyle, props) {
if (!userStyle) {
return null;
}
if (typeof userStyle === 'function') {
return userStyle(props);
}
return userStyle;
}
獲取用戶自定義的樣式。
接著進入到組件的定義部分,該組件職責很單一,負責UI的展示,所以狀態函數很少,只有render函數
render() {
const {
theme,
isPlaying,
markers: userMarkers,
style,
children,
currentTime,
lookAhead,
endTime
} = this.props;
const lookAheadTime = Math.min(endTime, currentTime + lookAhead);
const markers = userMarkers.concat({
time: lookAheadTime,
style: lookAheadMarkerStyle({theme, isPlaying, userStyle: style.lookAheadMarker})
});
return (
<PlaybackControl {...this.props} markers={markers}>
{children}
<div style={{flexGrow: 1}} />
{this._renderLookAheadSlider()}
</PlaybackControl>
);
}
首先通過比較結束時間與當前時間和前至時間的大小,獲得當前前至到什么時刻。
設置前至marker屬性,主要是時間及樣式,為PlaybackControl組件的markers屬性準備好數據。
最后將PlaybackControl組件和前至進度條組合后返回。
這里用到了_renderLookAheadSlider函數,來渲染前至進度條。
_renderLookAheadSlider() {
const {theme, style, isPlaying, lookAhead, formatLookAhead, maxLookAhead, step} = this.props;
return (
<LookAheadContainer theme={theme} isPlaying={isPlaying} userStyle={style.lookAhead}>
<LookAheadTimestamp
theme={theme}
isPlaying={isPlaying}
userStyle={style.lookAheadTimestamp}
>
Look ahead: {formatLookAhead(lookAhead)}
</LookAheadTimestamp>
<Slider
style={style.lookAheadSlider}
value={lookAhead}
min={0}
max={maxLookAhead}
step={step}
size={16}
onChange={this.props.onLookAheadChange}
/>
</LookAheadContainer>
);
}
其構成也很簡單,由一個span和一個Slider構成,分別存放文本Look ahead: *s和Slider滑塊。
DualPlaybackControl組件就介紹到這,可以看出該組件是有個組合組件,將由streetscape.gl/monochrome中的PlaybackControl組件主進度條和由Slider組件前至進度條構成,同時用以承接streetscape.gl中邏輯組件PlaybackControl的屬性及事件。
那接下來就繼續深挖,看看streetscape.gl/monochrome為我們提供的基礎組件——PlaybackControl和Slider。
先寫到這,待更新!