This article was originally published at: github.com/bigo-fronte… Welcome to follow and reprint.

Demand analysis:

  • There are two main things, scrolling, and visual range
  • Scrolling is provided by a component that receives two parameters from and to. The component realizes the transformation from numeric from to numeric to by scrolling up and down
  • Visual range judgment means that the component starts scrolling when it is in visual range
  • The remaining implementation requirements include that when the component is rendered, a change in the number to triggers the next scroll, implemented in VUE
  • The effect is shown below.

Implementation scheme:

  • Firstly, the requirement is disassembled. 999 can be regarded as a number composed of three nines, so the rolling of a string of digits is actually equivalent to the combination of a single digit rolling. Therefore, the rolling effect of a single digit can be realized first, and then assembled into components to achieve the desired effect
  • The scrolling of individual numbers is somewhat similar to the turning of pages on a mechanical clock. The realization is that there are other numbers on the back of the existing display number, and when appropriate, the machine controls the turning of the new number, and the old number is covered under the new number
  • So a single digital scroll, you can also use a similar approach, namely preparation from 0 to 90 Numbers, vertical arrangement, but the size of the visible range is only a number, called it a window, the 10 Numbers are based on window orientation, in the digital change, to change its position, and add animation effects, can achieve the effect of rolling

style

  • Based on the above scheme, we can write the following style layout
/* html */
  <div class="_single-digi-scroller">
    <div class="placeholder" ref="myPlaceHolder">0</div>
    <div class="display-panel" ref="myDigiPanel">
      <div class="num">0</div>
      <div class="num">1</div>
      <div class="num">2</div>
      <div class="num">3</div>
      <div class="num">4</div>
      <div class="num">5</div>
      <div class="num">6</div>
      <div class="num">7</div>
      <div class="num">8</div>
      <div class="num">9</div>
    </div>
  </div>

/* style */
._single-digi-scroller {
  display: inline-block;
  position: relative;
  overflow: hidden;
  .placeholder {
    opacity: 0;
  }
  .display-panel {
    position: absolute; }}Copy the code
  • Pay attention to heredisplay: inline-block;Is so that multiple components can be displayed side by side without wrapping,overflow: hidden;Is to ensure that the viewable range, that is, the window mentioned above, is only one number in size
  • Also of note, due to the rolling areadisplay-panelIs absolutely positioned, which causes the height of its parent element to collapse to zero, so HTML adds a transparentplaceholder, which is used to spread the height of the component so that we don’t have to set the height of the component to match the text height of the parent element
  • Now that the basic single-digit scroll component structure is written, add the scroll logic to it

Scroll to logic

  • According to the above style, we just need to changedisplay-panelTo change its position relative to the window, plustransitionThe CSS style will have an animated effect to achieve scrolling
  • First we need to know how to roll the number from what to what, so we need to add two props, from and to, and then calculate top based on from and to, as follows:
  props: {
    from: {
      type: [Number.String].default: 0
    },
    to: {
      type: [Number.String].default: 0
    },
    height: {
      type: [Number.String].default: 0
    },
    speed: {
      type: [Number.String].default: 2}},data: () = > ({
    toPos: false.fromPos: false.transitionStyle: {}}),watch: {
    changeInfo: {
      immediate: true.handler(val) {
        if (val) {
          this.fromPos = { top: ` -${val.from * this.height}px` };
          setTimeout(() = > {
            // Nexttick is not used because the spacing is too small and the browser changed pos before rendering, so the animation didn't work
            this.toPos = { top: ` -${val.to * this.height}px` };
            this.transitionStyle = { transition: `The ${this.speed}s` };
          }, 200); }}}},computed: {
    numStyle() {
      return {
        height: `The ${this.height}px`.lineHeight: `The ${this.height}px`
      };
    },
    panelStyle() {
      if (this.toPos) return{... this.toPos, ... this.transitionStyle };if (this.fromPos) return{... this.fromPos, ... this.transitionStyle };return {};
    },
    changeInfo() {
      if ((this.from || this.from === 0) && (this.to || this.to === 0)) {
        return { to: this.to, from: this.from };
      }
      return false; }}Copy the code
  • And you can see here that we’ve added onechangeInfoBecause we don’t know which one receives from or to first, so to make sure that when we compute the top attribute, both from and to are already computed, we add this attribute
  • thenchangeInfoWhen the value changes, the logic of watch will be triggered. First, the top attribute will be calculated according to from, and then passsetTimeoutThe callback calculates the top value corresponding to to and adds the animation style
  • Top value calculation is simple, the default window display is 0 at the beginning, when the top is 0, then if it is other number, only need to use this number multiplied by the height then invert, height the height of the window, that is the height of each number, such as 3 and 0, a difference of three Numbers, move up the height of the three Numbers window can display 3, i.etop = -3 * height
  • The above calculated values are finally summarizedpanelStyleIn this computed property, if there is a value corresponding to to, the value corresponding to to is taken first, and if there is no value corresponding to from, the value corresponding to FROM must be taken first, then the value corresponding to TO must be taken later than from, and finally the value corresponding to to is takenpanelStyleAssigned todisplay-panel, the scrolling logic is complete
  • Notice what we’re using herethis.height, this value can be computedplaceholderThe clientHeight is directly obtained, but because we will later assemble multiple single-digit rolling together, if each single-digit rolling component calculates one side of the clientHeight, it will inevitably cause a waste of performance, and the single-digit height after assembly is necessarily the same, so it can be unified in the assembled component calculation, It is then passed to the individual single-digit scroll components as props
  • And to ensure thatdisplay-panelThe height of each number is the same. We calculate the numStyle property and assign it todisplay-panelEvery number in
    <div class="display-panel" :style="panelStyle" ref="myDigiPanel">
      <div class="num" :style="numStyle">0</div>
      <div class="num" :style="numStyle">1</div>. </div>Copy the code

The assembly

/* html */
  <div class="_digi-scroller" 
    :style="{ height: `${height}px`, width: `${ changeInfo ? width * changeInfo.to.length : 0}px` }"
    ref="myDigiScroller"
  >
    <div class="placeholder" ref="myPlaceHolder">0</div>
    <div :style="{ left: `-${width}px` }" class="digi-zone" v-if="changeInfo">
      <single-digi-scroller
        class="single-digi"
        v-for="(digi, index) in changeInfo.to"
        :key="changeInfo.to.length - index"
        :from="changeInfo.from[index]"
        :to="digi"
        :height="height"
      />
    </div>
  </div>

/* style */
._digi-scroller {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  .placeholder {
    display: inline-block;
    opacity: 0;
  }
  .digi-zone {
    position: relative; display: inline-block; }}Copy the code
  • single-digi-scrollerIs the single-digit scroll component, similar to the previous,placeholderIt is also used to hold up the height. The style is basically similar to that of the single-digit rolling component, except that display isinline-flex, mainly for inline vertical alignment,white-space: nowrap;No line breaks are guaranteed
  • And you can also see thatdigi-zoneIt has a left property on it becausedigi-zoneIs relative to the layout (so that the width of the component is supported), and as mentioned earlier it has a transparent placeholder to the left, so move it one digit to cover itdigi-zoneLocated at the beginning of the entire component
  • Notice at the same time heresingle-digi-scrollerThe key value of is the reverse order of its index, because VUE will reuse elements according to the key value, and when the number of numeric digits increases or decreases, the number of digits changes is the highest, that is, when 99 changes to 100, the number of digits is added by onesingle-digi-scrollerIf the key value is equal to index, the first and second elements will be reused, which visually changes from 990 to 100 (as shown below), instead of 099 to 100. Therefore, to ensure that the last two elements are reused, the key value should be in reverse order
  • Let’s look at the calculation of each property one by one
  • Height and width can be computed directlyplaceholderIs the height and width value of the property in data, which is calculated when the component is first rendered
  mounted() {
    const { clientHeight, clientWidth } = this.$refs.myPlaceHolder;
    this.height = clientHeight;
    this.width = clientWidth;
  },
Copy the code
  • Width is the width of a single digit used to calculate the length of the entire component. The number of single-digit scroll components is determined bychangeInfoChangeInfo is calculated from the received arguments from and to, as shown below
  props: {
    from: {
      type: [Number.String].default: 0
    },
    to: {
      type: [Number.String].default: 0}},computed: {
    changeInfo() {
      if ((this.from || this.from === 0) && (this.to || this.to === 0)) {
        // After receiving both from and to
        const len = Math.max(String(this.to).length, String(this.from).length);
        // eslint-disable-next-line prefer-spread
        const from = `The ${Array.apply(null.Array(len)).map(() => '0').join(' ')}The ${this.from}`.slice(-len);
        // eslint-disable-next-line prefer-spread
        const to = `The ${Array.apply(null.Array(len)).map(() => '0').join(' ')}The ${this.to}`.slice(-len);
        return { from, to };
      }
      return false; }},Copy the code
  • The reason for setting changeInfo is to ensure that both from and to have been received. The second reason is that since the from and to digits are not necessarily the same, the longest digit needs to be calculated, and the shorter digit needs to be zeroed in front of it to ensure that the from and to digits in changeInfo are the same
  • Array.apply(null, Array(len))Is used to generate an array of len length and undefined elements
  • After this processing, we split from and to into single digits. Since the digits of from and to are the same after processing, we pass the corresponding digits into the single-digit rolling component respectively, and then the rolling effect can be enabled by the logic in the single-digit component
  • At this point, numerical scrolling is achieved by assembling multiple single-digit scroll components

Visual range judgment

  • In this case, IntersectionObserver with good performance can be used for implementation. For systems that do not support this method, the method of monitoring rolling can be adopted to cover the bottom
  • First determine whether the system supports itIntersectionObserver
  checkObserverSupport() {
    return 'IntersectionObserver' in window
     && 'IntersectionObserverEntry' in window
     && 'intersectionRatio' in window.IntersectionObserverEntry.prototype;
  }
Copy the code
  • A newinViewTo mark whether the current component has reached the viewable area, andlistenerIt is used to record the listening instance of IntersectionObserver and clear it when the page is destroyed. Therefore, the current data is as follows:
  data: () = > ({
    height: 0.width: 0.inView: false.listener: undefined.scrollTimer: undefined // scroll to monitor throttling
  })
Copy the code
  • Add a value in MountedcheckIntoViewMethod for supportIntersectionObserverMethod, by instantiating the method to judge the visual range, when the judge element reaches the visual range, clear the listener, taginviewSet to true and start countingchangeInfo, perform scroll
  checkIntoView() {
    if (this.checkObserverSupport()) {
      // eslint-disable-next-line no-unused-expressions
      this.listener && this.listener.disconnect();
      this.listener = new IntersectionObserver(
        entries= > {
          const { intersectionRatio } = entries[0];
          if (intersectionRatio > 0) {
            this.listener.disconnect();
            console.log('intersection observer: digi scroller into view');
            this.listener = undefined;
            this.inView = true; }});this.listener.observe(this.$refs.myDigiScroller);
    } else if (this.checkRectBounding()) {
      this.inView = true;
    } else {
      if (!this.scrollContainer) {
        this.scrollListenContainer = window;
      } else if (typeof this.scrollContainer === 'string') {
        this.scrollListenContainer = document.querySelector(this.scrollContainer);
      } else {
        this.scrollListenContainer = this.scrollContainer;
      }
      this.scrollListenContainer.removeEventListener('scroll'.this.checkIntoViewPollyfill);
      this.scrollListenContainer.addEventListener('scroll'.this.checkIntoViewPollyfill); }}/* computed */
  changeInfo() {
    if ((this.from || this.from === 0)
      && (this.to || this.to === 0)
      && this.inView // Only if this.inView is true will the calculation begin) {... }}Copy the code
  • For not supportedIntersectionObserverSystem, here was calculated in the first placecheckRectBounding, this method is used to determine whether the element is in the visual range if it is in the relative position of the page. The code is as follows. For those already in the visual range, scroll can be performed directly, while for those not yet in the visual range, scroll listening is performed
  checkRectBounding() {
    if (!this.$refs.myDigiScroller) return false;
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
    const rect = this.$refs.myDigiScroller.getBoundingClientRect() || {};
    const { top } = rect;
    return +top <= viewPortHeight + 100; // Because of throttling, the scope of determination is expanded a bit
  }
Copy the code
  • Go back tocheckIntoViewHere,this.scrollContainerIs a props property that allows the user to specify a container to listen on. Its initial value is null and it listens for window scrolling by default
  • checkIntoViewPollyfillThe code is as follows. In order to avoid repeated monitoring, remove operation is carried out before registering monitoring events. The essence of the monitoring method is to call checkRectBounding to determine whether the monitoring is in the visible range
  checkIntoViewPollyfill() {
    if (this.scrollTimer) return;
    const isInView = this.checkRectBounding();
    this.scrollTimer = setTimeout(() = > { this.scrollTimer = undefined; }, 100); / / throttling
    if (isInView) {
      console.log('scroll listener: digi scroller into view');
      this.scrollListenContainer.removeEventListener('scroll'.this.checkIntoViewPollyfill);
      // eslint-disable-next-line no-unused-expressions
      this.scrollTimer && clearTimeout(this.scrollTimer);
      this.scrollTimer = undefined;
      this.inView = true; }}Copy the code
  • Finally, when the page is destroyed, all listeners and timers are cleared to avoid impacting other pages
  beforeDestroy() {
    if (this.listener) {
      this.listener.disconnect();
      this.listener = undefined;
    }
    if (this.scrollListenContainer) {
      this.scrollListenContainer.removeEventListener('scroll'.this.checkIntoViewPollyfill);
    }
    // eslint-disable-next-line no-unused-expressions
    this.scrollTimer && clearTimeout(this.scrollTimer);
    this.scrollTimer = undefined;
  }
Copy the code

conclusion

  • In this way, a numerical scroll component with scroll listening capability is implemented, which only animates the scroll when the component is visible, providing a better user experience than a component that scrolls as soon as it is rendered, which can be placed anywhere on the page.
  • It can also be placed in a paragraph of text, because its height and font size are determined by its parent element, and it looks the same as plain text.

Welcome everyone to leave a message to discuss, wish smooth work, happy life!

I’m bigO front. See you next time.