3

I'm trying to build this sticky header navbar in my RN app. Basically, an horizontal scrollview of categories that highlight the current category based on Y scrolling.

Thanks to the video of great William Candillon (https://www.youtube.com/watch?v=xutPT1oZL2M&t=1369s) I'm pretty close, but I have a main problem.

I'm using interpolation to translate the X position of category View while scrolling. And then I have a Scrollview wrapping this Animated View. The problem is that Scrollview is not functional as is does not have the reference of the position of the Animated View. As you can see in the gif below (blue -> Animated.View / red -> Scrollview)

My Caption

I like the interpolation approach as it's declarative and runs on native thread, so I tried to avoid as much as possible create listener attached to scrollTo() function.

What approach would you consider?

export default ({ y, scrollView, tabs }) => {
  const index = new Value(0);

  const [measurements, setMeasurements] = useState(
    new Array(tabs.length).fill(0)
  );

  const indexTransition = withTransition(index);

  const width = interpolate(indexTransition, {
    inputRange: tabs.map((_, i) => i),
    outputRange: measurements
  });

  const translateX = interpolate(indexTransition, {
    inputRange: tabs.map((_tab, i) => i),
    outputRange: measurements.map((_, i) => {
      return (
        -1 *
          measurements
            .filter((_measurement, j) => j < i)
            .reduce((acc, m) => acc + m, 0) -
        8 * i
      );
    })
  });

  const style = {
    borderRadius: 24,
    backgroundColor: 'black',
    width,
    flex: 1
  };

  const maskElement = <Animated.View {...{ style }} />;

  useCode(
    () =>
      block(
        tabs.map((tab, i) =>
          cond(
            i === tabs.length - 1
              ? greaterOrEq(y, tab.anchor)
              : and(
                  greaterOrEq(y, tab.anchor),
                  lessOrEq(y, tabs[i + 1].anchor)
                ),
            set(index, i)
          )
        )
      ),
    [index, tabs, y]
  );
  return (
    <Animated.View style={[styles.container, {}]}>
      <Animated.ScrollView
        scrollEventThrottle={16}
        horizontal
        style={{ backgroundColor: 'red', flex: 1 }}
      >
        <Animated.View
          style={{
            transform: [{ translateX }],
            backgroundColor: 'blue'
          }}
        >
          <Tabs
            onPress={i => {
              if (scrollView) {
                scrollView.getNode().scrollTo({ y: tabs[i].anchor + 1 });
              }
            }}
            onMeasurement={(i, m) => {
              measurements[i] = m;
              setMeasurements([...measurements]);
            }}
            {...{ tabs, translateX }}
          />
        </Animated.View>
      </Animated.ScrollView>
    </Animated.View>
  );
};
Iván
  • 129
  • 4
  • ScrollView works fine, it does get the position of your Animated.View, it's only that Animated.View visually translates own X position afterwards. I don't see a reason to use both ScrollView and Animated.View. Do you want it to be scrollable by the user? Use ScrollView. Do you want it to be controlled outside by interpolation? Use Animated.View – Max Jan 07 '20 at 22:16
  • Well the thing is I would like to have it both a) Scrolled automatically by Y position and b) Scrolled manually by the user for a quick overview of the categories The thing is that translating the Animated.View makes impossible to manually scroll as the Scrollview does not have the reference of the Animated.View – Iván Jan 07 '20 at 22:36
  • what do you mean with "ScrollView does not have the reference of the Animated.View"? It doesn't need to have a reference for it, it just contains it. The fact that you can scroll your blue element on your gif proves it (othrewise you wouldn't be able to manually scroll it at all) – Max Jan 07 '20 at 22:38
  • Yes I mean the Scrollview contains the Animated.View but is scrollable as long as is not translated. When I translate the first element of the list to the right (so it is not visible in the screen), I cannot scroll back using the horizontal Scrollview, as it is 'outside' the boundaries of Scrollview. Does it make sense? – Iván Jan 07 '20 at 22:44
  • Yes I understand your problem. And trying to point out that this is the expected behavior - on react native, transform: translate behaves exactly the same as it does on the web - it only **visually** moves the element. It doesn't affect layout. It doesn't affect position inside ScrollView. No other elements know about transform: translate applied to the element. As far as ScrollView is concerned, on your gif, it is scrolled as far to the start as possible. It's not ScrollView's concern that your element, after being positioned by ScrollView, decides to offset itself X amount of pixels left – Max Jan 07 '20 at 22:49
  • Which is why there should only be one source of moving your element: either transform translate or being a children of scrollview – Max Jan 07 '20 at 22:50
  • Yes, that's basically the problem, so maybe the approach is not correct. Do you think any workaround that could work for both sources of movement. Maybe scrollTo() based on active index. – Iván Jan 07 '20 at 22:54
  • scrollTo and just the ScrollView is what I would have used. I don't see a reason to also keep Animated.View. It also doesn't make sense from usability standpoint, e.g. what would happen if you scroll manually and then scroll the vertical scrollview? Does one of it just stop scrolling tabs based on what was used last? Both sources of movement can't both be in control of one element – Max Jan 07 '20 at 22:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/205565/discussion-between-ivan-and-max). – Iván Jan 07 '20 at 23:09
  • remove `{flex:1}` from `Animated.ScrollView` – Ponleu Feb 28 '20 at 02:04

1 Answers1

0

For anyone facing this issue, I solved it by adding the following on the animated scrollview to auto scroll the to the active tab

// Tabs.tsx
 const scrollH = useRef<Animated.ScrollView>(null);
  
 let lastScrollX = new Animated.Value<number>(0);

 //Here's the magic code to scroll to active tab
 //translateX is the animated node value from the position of the active tab
 
  useCode(
    () => block(
      [cond(
        or(greaterThan(translateX, lastScrollX), lessThan(translateX, lastScrollX)),
        call([translateX], (tranX) => {
          if (scrollH.current && tranX[0] !== undefined) {
            scrollH.current.scrollTo({ x: tranX[0], animated: false });
          }
        })),
      set(lastScrollX, translateX)
      ])
    , [translateX]);

// Render the Animated.ScrollView
  return (
    <Animated.ScrollView
      horizontal
      ref={scrollH}
      showsHorizontalScrollIndicator={false}
      >{tabs.map((tab, index) => (
    <Tab ..../> ..... </Animated.ScrollView>
Mhisham
  • 564
  • 4
  • 9