In the requirement, I need to implement a countdown effect, which is similar to the effect of a scroll wheel



Analysis results:

  1. Implement a scroll wheel effect, with the container rolling up
  2. Multiple roller linkage can be realized

Seamless rolling

The container lists all the possible values to achieve the above effect, which is to change the offset value of the container at uniform speed. This scheme is similar to the seamless rotation scheme. Here is the scheme

<div className={styles.slider}>
  <div className={styles.pager_tape} style={{
    transform: `translateY(${percent * - 10`}}} %) >
    {
      Array.from({length: 10}).map((_, index) => (
        <span className={styles.ele}>{index}</span>))}</div>
</div>
<script>
const [percent, setPercent] = useState(0);
useInterval(() = > {
  setPercent(prev= > (prev + 1) % 10);
}, 1000)
</script>
<style>
.slider {
  height: 36px;
  width: 20px;
  overflow: hidden;
  position: relative;
}

.pager_tape {
  display: flex;
  flex-direction: column;
  position: absolute;
  top: 0;
  transition: transform 500ms;
}
.ele {
  font-size: 26px;
  line-height: 36px;
}
</style>
Copy the code

In the above code, we control the number displayed by setting the container’s offset position (see this article for the useInterval code)Timer in hooks use and reload). One problem with the above code is that when we go from 9 to 0, the container offset goes straight from -90% to 0%. But with a fixed transition time, you see something like this:



When the container reaches 9, continue to scroll to the copied 0. In this case, set the container’s offset position to 0% and control the animation of the containertransition-timefor0.

The specific code is as follows:

<div className={styles.slider}>
  <div className={styles.pager_tape} onTransitionEnd={endPlay} style={{
    transition: playing ? 'transform 500ms' :"',transform: `translateY(${percent * - 36}px) `}} >
    {
      Array.from({length: 10}).map((_, index) => (
        <span className={styles.ele}>{index}</span>))}<span className={styles.ele}>0</span>
  </div>
</div>
<script>
export function useInterval(callback, delay) {
  const savedCallback = useRef(() = > {});

  useEffect(() = > {
    savedCallback.current = callback;
  });

  useEffect(() = > {
    if(delay ! = =null) {
      const interval = setInterval(() = > savedCallback.current(), delay || 0);
      return () = > clearInterval(interval);
    }

    return undefined;
  }, [delay]);
}
const [percent, setPercent] = useState(0);
const [playing, setPlaying] = useState(true);
useInterval(() = > {
  let num = (percent + 1) % 11;
  let status = true;
  setPercent(num);
  setPlaying(status)
}, 1000)
const endPlay = () = > {
  if (percent+1= = =11) {
    setPercent(0);
    setPlaying(false); }};</script>
Copy the code

In the above code, copy a copy of 0 to 9 behind, in addition to the excessive animation time from JS to control, rather than using write fixed CSS.

  1. Declare a variableplayingTo control whether there is a transition animation
  2. Listening containertransitionEndThe event
  3. Checks whether the current scroll to9At the back of the0If so, it is calledendPlaymethods
  4. endPlayThe main function is to reset the offset position to 0 and set toplayingforfalse

In this way, for the user, there is no previous jump process, the scrolling effect is very smooth.

Implemented using two elements

In fact, we don’t need so many children. Look carefully at the effect of the GIF. There are actually only two elements in the viewport, one with the current value and the other with the value of (current + 1) % 11. Here is the DOM structure after adjustment:

<div className="slider">
  {[prev, cur].map((item, index) = > (
    <span key={index} className={`slider-textThe ${playing && 'slider-ani'} `} >
      {item}
    </span>
  ))}
</div>
Copy the code

Prev represents the previous value, and cur represents the current value. Let’s encapsulate it as a component

const { value } = props;
const [prev, setPrev] = useState(' ');
const [cur, setCur] = useState(' ');
const [playing, setPlaying] = useState(false);
const play = (prev, current) = > {
  setPrev(prev);
  setCur(current);
  setPlaying(false);
  setTimeout(() = > {
    setPlaying(true);
  }, 16);
};
useEffect(() = > {
  if (isEffective(value)) {
    play(cur, value);
  } else {
    setPrev(value);
    setCur(value);
  }
}, [value]);
Copy the code

UseEffect listens for value changes and only plays animation if the value is valid. Here is the CSS code:

.slider {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  height: 36px;
}

.slider-text {
  font-size: 26px;
  line-height: 36px;
  height: 100%;
  transform: translateY(0%);
}

.slider-ani {
  transform: translateY(-100%);
  transition: transform 500ms ease;
}
Copy the code

Describe the idea:

  1. Declare two variables to store the current value, prev: the previous value; Cur: indicates the changed value
  2. Listen for changes to incoming values
  • First pass: Sets prev and cur to the current passed values
  • Second value change: call the play method
  1. Set the value of prev to the cur value before the change, and change the cur value to the passed value
  2. Set playing to false and then set it to true 16ms later

There is a question about why we need to set playing to true after 16ms. You will understand after disassembling the process

  1. After the last move, playing is true, and the container’s offset position istranslateY(-100%)
  2. When a new value comes in, we need to change the value of the element and reset the container position totranslateY(0)Note that this process should not be animated
  3. 16ms, which is how long it takes to refresh each frame of the screen

If you want to achieve the effect at the beginning of this article, you just need multiple components next to each other.

Above is all about the rolling effect of the content, welcome to pay attention to my public number: good front-end learning