Building Dial Interaction in React Native Part — III

Karthik Balasubramanian
Timeless
Published in
5 min readApr 26

--

Hey Everyone! This has been my longest blog series. Well, I wanted to explain in detail the Interaction we built. Phew!

Up until now, you would have known how to construct/structure the Dial and Notches, map the circular gesture into angle. Check out my previous blog posts.

In this blog, we will use the already calculated currentAngle value and map it to the notches, provide haptic feedback and as a bonus add sound on every tick change.

Let us get started.

Cover Image by Fayas fs

Building the Notch Component

The final code for the Notch component is

type NotchesProps = {
index: number;
currentAngle: SharedValue<number>;
playSound: () => void;
};

const Notches = ({ index, currentAngle, playSound }: NotchesProps) => {
const hapticSelection = useHaptic();
const active = useSharedValue(0);

useAnimatedReaction(
() => currentAngle.value,
(next, _prev) => {
// Mapping current angle to an index value
let currentAngleFactor = next / angle;
// Mapping Notch angle to an index value
let notchAngleFactor = (index * angle) / angle;
// Setting the currentAngleFactor && notchAngleFactor to the notches when it is zero
// It is the 0, 360 degree point in the Knob
if (currentAngleFactor === 0) {
currentAngleFactor = notches;
}
if (notchAngleFactor === 0) {
notchAngleFactor = notches;
}

if (currentAngleFactor >= notchAngleFactor) {
if (active.value === 0) {
hapticSelection && runOnJS(hapticSelection)();
runOnJS(playSound)();
}
active.value = withSpring(1);
} else {
if (active.value === 1) {
hapticSelection && runOnJS(hapticSelection)();
runOnJS(playSound)();
}
active.value = withSpring(0);
}
},
);
const currentStrokeAngle = index * angle;
const animatedStyles = useAnimatedStyle(() => {
return {
opacity: interpolate(active.value, [0, 1], [0.45, 1]),
backgroundColor: interpolateColor(
active.value,
[0, 1],
["rgba(0,0,0,0.51)", "white"],
"RGB",
),
};
});
if (currentStrokeAngle === 0) {
return null;
}
return (
<Animated.View
key={index}
style={[
tailwind.style("absolute h-1 w-5 rounded-3xl"),
getTransform(currentStrokeAngle),
animatedStyles,
]}
/>
);
};

The main code snippet of the above component is the useAnimatedReaction(), this hook allows performing actions on SharedValue change. Check the Reanimated Docs to know more.

Here we use it to know when the currentAngle changes, on every change a callback function is invoked with the new and old values.

At first, we calculate two values, currentAngleFactor and notchAngleFactor.

The currentAngleFactor is the factor value of the current angle of the Knob/Dial.

The notchAngleFactor is the angle the Notch represents, ideally we multiply the index with the angle and divide it by the same value, so it is the index. (Seems like something to refactor).

The next set of “if” cases are for the initial state of the Dial. It is always active, so when the “factors” value is zero. We set the factors to total no. notches.

After finding out the factors, if the currentAngleFactor is greater than or equal to the notchAngleFactor, the active value is set to 1 (active is a SharedValue variable defined locally for each notch to store an active state of the Notch). This triggers an animation, which changes the opacity and background color of the Notch.

And when the currentAngleFactor is less than the notchAngleFactor, the active value is set to 0, making the Notch component return to the original state.

Feedback `onChange` of Angle

Now, there are two side effects which needs to take place for us.

  1. Haptic feedback
  2. Play sound

These two are functions run on the JavaScript thread, so we use the runOnJS() to execute them.

Let us first look at how to get the sound.

To get sound, we use the expo-av package.

Check the installation steps over here.

For this to work, we first create a React State to load and store the sound.

We picked the sound from https://snd.dev/.

Here is the code to load the sound and also the playSound().

const [localSound, setLocalSound] = useState<Audio.Sound>();
useEffect(() => {
const loadSound = async () => {
const { sound } = await Audio.Sound.createAsync(
require("../assets/tap_05.mp3"),
);
setLocalSound(sound);
};
loadSound();
}, []);

async function playSound() {
try {
if (localSound) {
// await localSound.setRateAsync(1.2, false);
await localSound.setPositionAsync(0);
await localSound.playAsync();
}
} catch (e) {}
}

There is also stopSound() which is called on the end of the Pan Gesture.

const stopSound = () => {
localSound?.stopAsync();
};

The playSound() is passed to the <Notches /> component which plays the sound when the active value changes from 0 to 1 or vice versa.

Now for the haptics. I use the useHaptic() hook, which returns the default selection haptic feedback.

To know more about the hook, check out this blog.

Same as playSound() this function is also executed when the active value changes from 1 to 0 or vice versa.

All put together, we get a great UI Interaction.

Experience yourself from the Expo Snack.

Checkout the video here from the tweet:

The UI Interaction Tweet

Well, that brings us to the end of the blog series.

It was long, but I hope it was useful.

This blog was part of the UI Interaction, check out my other blogs here.

UI Interactions With React Native and Reanimated

15 stories

This is Karthik from Timeless

I hope you found this blog post helpful and informative. If you have any feedback or suggestions, please leave a comment below. I’d love to hear your thoughts and opinions on the topic.

And if you have any ideas for future blog posts, let me know! I’m always looking for new and interesting topics to explore.

Thank you for reading, and I look forward to hearing from you. :)

--

--