- clone repo
git clone https://github.com/callstack-internal/delightful-ux-training-app
- install dependencies
yarn install
- run expo app
yarn start
- open App on device or simulator
- for Android - open app on the device and scan QR code
- for iOS - type app ip in Safari browser on iOS device
- Android/iOS simulator - press "Run on iOS simulator"/"Run on Android device/emulator" in Expo web tools
Reanimated documentation: https://github.com/kmagiera/react-native-reanimated
Install Reanimated:
expo install react-native-reanimated
Create a function to animate FavouriteButton
opacity smoothly.
In utils/animationHelpers
:
- Create function returning
block
-runLinearTiming
. runLinearTiming
should acceptclock
,toValue
(value which should be at the end of the animation) andduration
.- Start with preparing
state = { finished, frameTime, position, time }
andconfig = { toValue, duration, easing }
in the function body. block
you'll return should:- Check if clock is running (
cond
,clockRunning
), - If not yet, reset the clock (
set
) and the state, updateconfig.toValue
and start the clock (startClock
), - Run
timing
usingclock
,state
andconfig
we already have, - Check if clock is finished, if positive, stop it (
stopClock
), - Call (and return at the same time)
state.position
.
- Check if clock is running (
In FavouriteButton
:
- Use
Animated.Value
, let's call ittoValue
. - Use
Clock
,clock
. - Assign newly created
runLinearTiming
toopacity
class property. - Apply value we just animated to
opacity
style inrender
. - Remember to update manually
toValue
when component updates (ComponentDidUpdate
). - Remember to use
Animated.View
!
In Player
:
- In body class create new
playingState
Animated.Value
. - In
handlePlayToggle
toggleplayingState
value between 1 and 0. UsesetValue
,cond
andeq
. - Pass
playingState
prop to thePlayPauseButton
.
In PlayPauseButton
:
- Toggle
opacity
of play and pause buttons. - Toggle
rotation
of the container. - Use
runLinearTiming
again onpauseOpacity
.toValue
argument should equalisPlaying
prop. - You can use
this.prop.isPlaying
prop directly inrunLinearTiming
- as it evaluates to1
or0
. - Use
interpolate
to getplayOpacity
androtateY
(from 0 to 180). - Use
concat
node to add 'deg' sufix to therotateY
. - Use opacity and rotate values in
transform
style of proper elements.
If you have some time now, try to implement a progress bar! A progress bar should:
- animate or stop when play or pause is pressed - translateX property of progress bar indicator
- animate equally long to current song duration
- resume animation from the position where stopped
- reset when song changes
You will need:
block
,eq
,set
,cond
- math functions -
multiply
,divide
,sub
runLinearTiming
function we created beforeValue
andClock
- of course
In SongList
:
- First create
AnimatedFlatList
usingAnimated.createAnimatedComponent
and put it in a place of regularFlatList
- it will provide more props for us. - Uncomment
HeaderTitle
. - Create
scrollY
value before thereturn
(Animated.Value
) - Set
scrollEventThrottle
prop of theAnimatedFlatList
to 16 - this will provide smooth performance. - Use
AnimatedFlatList
'sonScroll
event. - Extract
nativeEvent.contentOffset.y
value from theevent
and save it to thescrollY
. - Pass
scrollY
toCollapsibleHeader
andHeaderTitle
components.
In CollapsibleHeader
:
- Interpolate
translateY
([0, 130] => [0, 130]),opacity
([0, 200] => [1, 0]) andscale
([0, 130] => [1, 0.6]) values basing onscrollY
prop. - Add
Extrapolate.CLAMP
to the interpolations - you don't want to exceed the range. - Change regular
View
toAnimated.View
where necessary. - Use interpolated values in styles.
- In
imageContainer
styles apply translateY equal tothis.translateY
* 0.3 (usemultiply
). shadowContainer
should have height equal tothis.translateY
.
In HeaderTitle
:
- Interpolate
titleOpacity
class field ([50, 100] => [0, 1]) basing onscrollY
prop. - Add
Extrapolate.CLAMP
to the interpolation. - Change regular
View
toAnimated.View
where necessary. - Use interpolated value in styles.
Documentation for RN Gesture Handler: https://kmagiera.github.io/react-native-gesture-handler/
Install Gesture Handler:
expo install react-native-gesture-handler
Work in SongItem
:
- Remember to change song
container
fromView
toAnimated.View
- you can't animate regular View, right? - Wrap
Animated.View
usingPanGestureHandler
. - Use
activeOffsetX
prop to allow only swipe right. - Use
maxPointers
prop to set number of fingers required for the gesture. - Create class constructor.
- In the constructor create
onGestureEvent
class field. Similar way to flatlist scroll, assignAnimated.event
to it and extracttranslationX
from theevent
in the function. - Assign
this.onGestureEvent
toonGestureEvent
andonHandlerStateChange
prop ofPanGestureHandler
. - Use
translationX
inAnimated.Value
style.
In SongItem
:
- Create
const dragX
- Animated Value - andthis.gestureState
Animated Value equal toState.UNDETERMINED
in the constructor. - Create
const springClock
-Animated.Clock
- in the constructor. - Extract also
state
from theevent
. - Reassign
translationX
from theevent
- save it to our newdragX
helper. - Assign
cond
to thetranslationX
. - Check if the gesture is still active (use
cond
,eq
,State.ACTIVE
). - If is active, stop clocks and return
dragX
. - If not active, animate back to the start position - run
runSpring
function you'll prepare in a moment. Call it withspringClock
anddragX
arguments.
In utils/animationHelpers
:
- Create
runSpring
function withclock
andposition
arguments. - In the function body prepare
state
object containingfinished
,velocity
,position
andtime
values. - Prepare config object using
SpringUtils.makeDefaultConfig()
. - Return following
block
:- If
clockRunning
(cond
), reset state, restorestate.position
andstartClock
. - Run
spring
withclock
,state
andconfig
arguments. - If
state.finished
(cond
),stopClock
. - Return
state.position
.
- If
In SongItem
:
- In the constructor create new
Animated.Value
equal to importedROW_HEIGHT
. Let's call it justthis.height
. - In the constructor create helper
const dragVelocityX
- Animated Value. - Exctract
velocityX
from theevent
. - Create 2 new
clocks
in the constructor:clock
andswipeClock
. - Stop those 2
clocks
in thecond
we have for checking if the gesture isactive
. - In the
cond
already assigned to thetranslationX
nest anothercond
- check if gesture passed 80 breakpoint (greaterThan
) (in a place of currentrunSpring
call). - If it didn't, it should revert as before (call
runSpring
here). - If succeeded, call block containing 2 functions:
runLinearTiming
to animateSongItem
height to 0. Yourposition
argument will bethis.height
.runSwipeDecay
you'll create in a moment. Call it withswipeClock
,dragX
anddragVelocityX
arguments.
- Apply
this.height
tosong
View style. - For nice effect, create
opacity
Animated Value in constuctor andinterpolate
its value basing onthis.height
. - Apply
this.opacity
to the styles ofsong
.
In utils/animationHelpers
:
- Create
runSwipeDecay
function. It should acceptclock
,value
andvelocity
arguments. - In the function body create :
state
object containingfinished
,velocity
,position
andtime
valuesconfig
object containingdeceleration
value equal to 0.99
- Return
block
containing:- Checking if
clockRunning
; if false runblock
, in which resetstate
andconfig
and runstartClock
. - Calling
decay
withclock
,state
andconfig
arguments. - Checking if
state.finished
; if true,stopClock
. - Returning
state.position
.
- Checking if
Remove the song from the state if hidden (branch 7-remove-song
)
In SongItem
:
- You already implemented
runLinearTiming
if the gesture passed the breakpoint; add another key to that function config:callback
with valuethis.handleHideEnd
.
In utils/animationHelpers
:
- Add another key to the config object - function argument -
callback
. - Set default value to
() => {}
, so it won't break the function if called withoutcallback
. - To the last
cond
in theblock
you return add callback call. Insert it to the sameblock
asstopClock
. - Call callback using
call
node with arguments[state.finished]
(must be an array; when any of the array values updates, the call will be triggered) andcallback
.
Since now we'll be working on Login
screen. To make it more comfortable, edit state.showLoginScreen
in Home
, so the Login
screen will be initially visible.
Documentation for the theme provider: https://github.com/callstack/react-theme-provider
- Install theme provider:
yarn add @callstack/react-theme-provider
In utils/theming
:
- Create
ThemeProvider
andwithTheme
usingcreateTheming
method.
In App
:
- Import
ThemeProvider
and 2 themes. - Wrap main entry point using
ThemeProvider
. - Pass
theme
prop to theThemeProvider
- this will help us to toggle theme in the app.
In Login
:
- Import
withTheme
HOC. - Wrap exported component with the HOC.
- Edit
styles
to method - it should consumetheme
prop and return computed style using theme values. - Change hardcoded color values to these from
theme
(e.g.theme.primaryTextColor
).
Check if theme works using toggle!
i18n-js documentation: https://github.com/fnando/i18n-js Localization documentation: https://docs.expo.io/versions/latest/sdk/localization/
Install both libraries:
yarn add i18n-js
expo install expo-localization
- Import
i18n-js
,expo-localization
and translation objects (fromutils/translations
). - Assign
Localization.locale
toi18n.locale
. - Assign translations to locales - create object and pass it to
i18n.translations
. - Allow fallbacks using
i18n.fallbacks
. - Set default locale using
i18n.defaultLocale
. - Insert dynamic strings using
i18n.t()
method.
Test it out changing settings of your emulator or hardcoding i18n.locale
!
- Import
I18nManager
. - Create condition under which the layout should be RTL (
isRTL
variable you can use later). - Set
I18nManager.allowRTL
andI18nManager.forceRTL
. - Test RTL layout and adjust styles if neccessary using
isRTL
.
Documentation: https://facebook.github.io/react-native/docs/accessibility
First, prepare your simulator / device (warning - for now you can't test in on iOS simulator).
For the emulator:
- Download apk from http://tiny.cc/androidreader
- Drop the apk to the emulator
On emulator / device go to:
- Settings
- Accessibility
- TalkBack
- Use service
On device go to:
- Settings
- General
- Accessibility
- Vision
- VoiceOver
- Apply
accessibilityLabel
,accessibilityHint
where apropriate. - You can also use
i18n.t()
to make it internationalized! - Hide labels next to
Toggle
components usingaccessibilityElementsHidden
andimportantForAccessibility
- to serve both platforms.
If you have some spare time, try to create one accessible
element in a place of few visual ones!