Skip to main content

Use Acrool React Carousel to Juejin a level Carousel

· 4 min read
Imagine Chiu
Front End Engineer @ Acrool

I'm using the open-source library I previously developed, Acrool React Carousel, to see how far I can replicate the functionality of the membership level carousel from the Juejin app. We had similar requirements at my previous company. Without further ado, let's get started.

Requirements

First, let's take a look at the preview results of Juejin on the mobile app

Juejin member level carousel

  • The scrollable area is limited to the card section.
  • When scrolling, the level cards should move upwards.
  • The selected level card should be centered.
  • The area below the level card should be partially obscured with a curved shape, not fully visible.
  • The level names should move synchronously with the cards.
  • As the level names move, the curved shape should also move.
  • The level name lines should move in sync.
  • When retrieving data from the API, the preset should be positioned at the selected level without any animated movement.

The carousel project can be divided into:

  • Level cards, displaying 1.2 of them and centering.
<AcroolCarousel
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>
  • Level names
  • level lines both of which should display 3 and be centered.
<AcroolCarousel
slidesPerView={3}
isCenteredSlides={true}
// ...ignore
/>

So the development goal is to synchronize the movement of level names and level lines when moving the level cards.

import React, {useCallback, useEffect, useRef, useState} from 'react';
import AcroolCarousel, {TMoveEffectFn, TAcroolSlideItemDataList, AcroolSlideCard, elClassName, Controller, TOnSlideChange} from '@acrool/react-carousel';

const MemberLevelWrapper = () => {
const carouselMainRef = useRef<AcroolCarousel>(null);
const carouselMetaRef = useRef<AcroolCarousel>(null);
const carouselLineRef = useRef<AcroolCarousel>(null);

return <>
{/* Level Card */}
<AcroolCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef, carouselLineRef]}
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>

{/* Level Name*/}
<AcroolCarousel
ref={carouselMetaRef}
// ...ignore
/>

{/* Level Line */}
<AcroolCarousel
ref={carouselLineRef}
// ...ignore
/>
</>;
};

Next, when scrolling, the level cards will move upwards, and we need to implement a new Animation Effect function

import AcroolCarousel, {TMoveEffectFn} from '@acrool/react-carousel';

const mainMoveEffectFn: TMoveEffectFn = useCallback((percentageInfo) => {
const transformY = 40;
return {
transform: `translate(0px, ${-transformY * (percentageInfo.calcPercentage - 1)}px)`,
};
}, []);

<AcroolCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef, carouselLineRef]}
moveEffect={{
moveFn: mainMoveEffectFn,
}}
// ...ignore
/>

The level names should move in an arched pattern.

const levelNameMoveEffectFn: TMoveEffectFn = useCallback((percentageInfo) => {
const transformY = -19;
return {
transform: `translate(0px, ${-transformY * (percentageInfo.calcPercentage - 1)}px)`,
};
}, []);

<AcroolCarousel
ref={carouselMainRef}
moveEffect={{
moveFn: levelNameMoveEffectFn,
}}
// ...ignore
/>

As for the arched section, we will ultimately use a masking technique for display. We should also disable NavButton, Pagination, and MouseMove. If you're not familiar with SVG drawing, you can use this tool.

svg-path-editor

SVG Mask for level line

<LevelLine>
<LineAcroolCarousel
ref={carouselLineRef}
data={lineData}
slidesPerView={3}
isCenteredSlides={true}
isEnableNavButton={false}
isEnablePagination={false}
isEnableMouseMove={false}
/>

<svg height="100%" width="100%">
<clipPath id="wave12">
{/*跟隨線*/}
<path d="M 0 4 C 175 30 175 30 356 4 L 356 2 C 175 28 175 28 0 2" stroke="black" fill="transparent"/>
</clipPath>
</svg>
</LevelLine>

SVG Mask Result fro level line

The masking for the Level Card should be the same, but it's divided into three sections.

SVG Mask 1 for Level Card SVG Mask 2 for Level Card SVG Mask 3 for Level Card

<svg height="100%" width="100%">
<clipPath id="wave10">
<path d="M 0,0 356,0 356,130 0,130" stroke="black" fill="transparent"/>
{/* 圓弧 */}
<path d="M 0 130 C 175 155 175 155 356 130" stroke="black" fill="transparent"/>
{/* 下箭頭 */}
<path d="M 152 143 L 176 153 L 178 153 L 202 143" stroke="black" fill="transparent"/>
</clipPath>
</svg>

SVG Mask Result for Level Card

Lastly, the final part is moving to the preset selected level without any animation.

const [carouselMainController, setMainController] = useState<Controller>();
const [currLevel, setCurrLevel] = useState<{lv: number,count: number}|undefined>();


useEffect(() => {
carouselMainController?.slideToPage(5, false);
}, [carouselMainController]);


<AcroolCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef]}
onSlideChange={handleSlideChange}
setController={setMainController}
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>

The finished result

Finished Demo

The current progress is presented as a file for demonstration. If you'd like to try it out, you can visit acrool-react-carousel.pages.dev. That's pretty much it. If you need both sides to move, they sync with each other in the Acrool React Carousel. Because they are independently synchronized, there won't be circular control. It means A controls B and C, but B doesn't automatically control C. So, you can see here that A controls B and C.