streetscape.gl学习笔记(三)

很久没有更新博客了,原因总有许多。但是每每都会回来看看有没有信息,看到写文章中还有不少只写了开头未发布的文章,心中又有不舍。这是关于streetscape.gl的第三篇笔记,或许后面还会有更新,亦或许没有更新换做其他领域的文章。不管怎么说,把streetscape.gl的一个闭环讲完也算是了却自己心头一件事。
这篇主要讲进度条,在各类看板中进度条是必备的。播放控制、滑块定位、刻度展示等等。当然我们也可以以此为模板进行扩展,例如:添加倍速、标记关键时间节点、添加前至进度条等等。在对历史数据回放时,汽车工程师和进度条之间的交互就愈发频繁,所以进度条准确、便利给客户带来的感受提升不亚于在地图页面。
streetscape.gl demo上的进度条由两部分组成:一个我称之为主进度条、另一个我管它叫前至进度条(举个例子:当前主进度条播放到10s处,如果前至进度条设置为2s,则在LogViewer中不仅能看到10s处的数据也可以看到12s处的数据。为什么有这样的功能,可能只有客户知道吧)。当然streetscape.gl是通过组合的方式提供的一个组件。


image.png

如何使用

在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组件各状态流程(不能烂熟于心只能一一对照着看了)


react.png

先看看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是 XVIZStreamLoaderXVIZLiveLoaderXVIZFileLoader中的一种,其都继承于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。


先写到这,待更新!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容