streetscape.gl學(xué)習(xí)筆記(三)

很久沒(méi)有更新博客了,原因總有許多。但是每每都會(huì)回來(lái)看看有沒(méi)有信息,看到寫(xiě)文章中還有不少只寫(xiě)了開(kāi)頭未發(fā)布的文章,心中又有不舍。這是關(guān)于streetscape.gl的第三篇筆記,或許后面還會(huì)有更新,亦或許沒(méi)有更新?lián)Q做其他領(lǐng)域的文章。不管怎么說(shuō),把streetscape.gl的一個(gè)閉環(huán)講完也算是了卻自己心頭一件事。
這篇主要講進(jìn)度條,在各類看板中進(jìn)度條是必備的。播放控制、滑塊定位、刻度展示等等。當(dāng)然我們也可以以此為模板進(jìn)行擴(kuò)展,例如:添加倍速、標(biāo)記關(guān)鍵時(shí)間節(jié)點(diǎn)、添加前至進(jìn)度條等等。在對(duì)歷史數(shù)據(jù)回放時(shí),汽車工程師和進(jìn)度條之間的交互就愈發(fā)頻繁,所以進(jìn)度條準(zhǔn)確、便利給客戶帶來(lái)的感受提升不亞于在地圖頁(yè)面。
streetscape.gl demo上的進(jìn)度條由兩部分組成:一個(gè)我稱之為主進(jìn)度條、另一個(gè)我管它叫前至進(jìn)度條(舉個(gè)例子:當(dāng)前主進(jìn)度條播放到10s處,如果前至進(jìn)度條設(shè)置為2s,則在LogViewer中不僅能看到10s處的數(shù)據(jù)也可以看到12s處的數(shù)據(jù)。為什么有這樣的功能,可能只有客戶知道吧)。當(dāng)然streetscape.gl是通過(guò)組合的方式提供的一個(gè)組件。


image.png

如何使用

在streetscape.gl提供的get-started我們可以看到它是這樣被加入進(jìn)來(lái)的。

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>
    ...
    );
  }

可以看到使用方便,只要輸入相關(guān)屬性即可。

如何實(shí)現(xiàn)

在./modules/core/src/components/playback-control中,我們可以看到這個(gè)組件的定義。其中index.js文件定義的PlaybackControl組件,我們可以視其為容器組件,主要負(fù)責(zé)組件的邏輯部分;而dual-playback-control.js文件定義的DualPlaybackControl組件,其組合了streetscape.gl/monochrome中的PlaybackControl組件,我們可以視其為UI組件,主要負(fù)責(zé)頁(yè)面渲染部分。
先看index.js中PlaybackControl組件

class PlaybackControl extends PureComponent {
...
}

這里插入一下PureComponent和Component之間的區(qū)別。
得益于作者這兩天看的《React狀態(tài)管理及同構(gòu)實(shí)戰(zhàn)》這本書(shū),對(duì)React中的部分細(xì)節(jié)有所感悟,邊看該框架的同時(shí),也看看它運(yùn)用React的最佳實(shí)踐,精華部分當(dāng)然應(yīng)該吸收借鑒。
一句話概括就是:PureComponent通過(guò)props和state的淺對(duì)比來(lái)實(shí)現(xiàn) shouldComponentUpate(),而Component沒(méi)有,而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
  };

從屬性的定義上,我們可以看到,有部分屬性來(lái)自log以獲得這批數(shù)據(jù)的起止時(shí)間、時(shí)間間隔、前至多久等,還有當(dāng)前播放狀態(tài),以及在頁(yè)面上的樣式用以顯示。
我們從React幾個(gè)狀態(tài)函數(shù)入手看看,組件各個(gè)階段都做了什么。先存一張圖,react組件各狀態(tài)流程(不能爛熟于心只能一一對(duì)照著看了)


react.png

先看看componentWillReceiveProps

  componentWillReceiveProps(nextProps) {
    if ('isPlaying' in nextProps) {
      this.setState({isPlaying: Boolean(nextProps.isPlaying)});
    }
  }

當(dāng)組件運(yùn)行時(shí)接到新屬性,根據(jù)新屬性更新播放狀態(tài)。這個(gè)相對(duì)單一,再看看componentDidUpdate

  componentDidUpdate(prevProps, prevState) {
    const {isPlaying} = this.state;
    if (isPlaying && prevState.isPlaying !== isPlaying) {
      this._lastAnimationUpdate = Date.now();
      this._animationFrame = window.requestAnimationFrame(this._animate);
    }
  }

當(dāng)狀態(tài)或者屬性發(fā)生改變,引起組件的重新渲染并觸發(fā)。
當(dāng)播放狀態(tài)為true同時(shí)上一播放狀態(tài)為false時(shí),讓瀏覽器在下一次重繪之前執(zhí)行_animate動(dòng)畫(huà),同時(shí)它返回一個(gè)long值_animationFrame,是回調(diào)列表中唯一的標(biāo)識(shí)。是個(gè)非零值,沒(méi)別的意義。你可以傳這個(gè)值給 window.cancelAnimationFrame() 以取消回調(diào)函數(shù)。
再看看_animate這個(gè)動(dòng)畫(huà)

  _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);
    }
  };

當(dāng)播放狀態(tài)為true時(shí),開(kāi)始執(zhí)行該動(dòng)畫(huà)。
先計(jì)算當(dāng)前時(shí)間和最后一次更新時(shí)間的時(shí)間差timeDeltaMs。
通過(guò)時(shí)間差timeDeltaMs和屬性中的該log對(duì)應(yīng)的timestamp求和獲得新的newTimestamp。
檢查buffered中是否有區(qū)間正好卡住newTimestamp,如果有則向前移動(dòng)否則就暫停并等待。(這段估計(jì)是為了控制進(jìn)度條中的slider)。
設(shè)置最近的動(dòng)畫(huà)更新時(shí)間點(diǎn)為now。
為了在瀏覽器下次重繪之前繼續(xù)更新下一幀動(dòng)畫(huà),那么回調(diào)函數(shù)_animate中自身必須再次調(diào)用window.requestAnimationFrame()。
這里調(diào)用了一個(gè)_onTimeChange函數(shù)

  _onTimeChange = timestamp => {
    const {log, onSeek} = this.props;
    if (!onSeek(timestamp) && log) {
      log.seek(timestamp);
    }
  };

該函數(shù)主要是讓log數(shù)據(jù)seek到指定的timestamp。這里的log是 XVIZStreamLoaderXVIZLiveLoaderXVIZFileLoader中的一種,其都繼承于XVIZLoaderInterface這部分計(jì)劃再單開(kāi)一片筆記講解一下
再看看componentWillUnmount,這一階段主要將注冊(cè)的動(dòng)畫(huà)取消掉。

  componentWillUnmount() {
    if (this._animationFrame) {
      window.cancelAnimationFrame(this._animationFrame);
    }
  }

該組件的主要生命周期函數(shù)介紹完后,再看看render函數(shù)。

  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是否為有窮數(shù),如若不是直接返回為null。該部分有可能是針對(duì)Live數(shù)據(jù)源,對(duì)于直接是實(shí)時(shí)的數(shù)據(jù)源就不用安排上進(jìn)度條了。然后將屬性、事件,通過(guò)props向子組件傳遞,其中對(duì)bufferRange屬性值和全局的startTime、endTime進(jìn)行比較,使其落入到最小范圍區(qū)間內(nèi)。
接著又定義了一個(gè)getLogState的函數(shù),主要是從log數(shù)據(jù)體中獲得timestamp、lookAhead、startTime等數(shù)據(jù)。

const getLogState = log => ({
  timestamp: log.getCurrentTime(),
  lookAhead: log.getLookAhead(),
  startTime: log.getLogStartTime(),
  endTime: log.getLogEndTime(),
  buffered: log.getBufferedTimeRanges()
});

這個(gè)函數(shù)是用于connectToLog這個(gè)高階組件包裝函數(shù)中的。

export default connectToLog({getLogState, Component: PlaybackControl});

這里,作者把所有需要綁定log相應(yīng)屬性生成高階組件,將其抽出成一個(gè)高階組件生成函數(shù)——connectToLog,你可以在源碼的很多地方瞧見(jiàn)它。是一個(gè)很不錯(cuò)的實(shí)踐,可供參考。
這部分先講到這,再看看DualPlaybackControl這個(gè)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)
}));

先是定義了兩個(gè)dom的容器,一個(gè)用以存放前至進(jìn)度條,另一個(gè)用以存放前至多久數(shù)值。

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)
});

這部分用以定義主進(jìn)度條前至滑塊的樣式。其中evaluateStyle(在modules\monochrome\src\shared\theme.js文件中)函數(shù)定義如下

export function evaluateStyle(userStyle, props) {
  if (!userStyle) {
    return null;
  }
  if (typeof userStyle === 'function') {
    return userStyle(props);
  }
  return userStyle;
}

獲取用戶自定義的樣式。
接著進(jìn)入到組件的定義部分,該組件職責(zé)很單一,負(fù)責(zé)UI的展示,所以狀態(tài)函數(shù)很少,只有render函數(shù)

  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>
    );
  }

首先通過(guò)比較結(jié)束時(shí)間與當(dāng)前時(shí)間和前至?xí)r間的大小,獲得當(dāng)前前至到什么時(shí)刻。
設(shè)置前至marker屬性,主要是時(shí)間及樣式,為PlaybackControl組件的markers屬性準(zhǔn)備好數(shù)據(jù)。
最后將PlaybackControl組件和前至進(jìn)度條組合后返回。
這里用到了_renderLookAheadSlider函數(shù),來(lái)渲染前至進(jìn)度條。

  _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>
    );
  }

其構(gòu)成也很簡(jiǎn)單,由一個(gè)span和一個(gè)Slider構(gòu)成,分別存放文本Look ahead: *s和Slider滑塊。
DualPlaybackControl組件就介紹到這,可以看出該組件是有個(gè)組合組件,將由streetscape.gl/monochrome中的PlaybackControl組件主進(jìn)度條和由Slider組件前至進(jìn)度條構(gòu)成,同時(shí)用以承接streetscape.gl中邏輯組件PlaybackControl的屬性及事件。
那接下來(lái)就繼續(xù)深挖,看看streetscape.gl/monochrome為我們提供的基礎(chǔ)組件——PlaybackControl和Slider。


先寫(xiě)到這,待更新!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容