delightful-ux-training-app
delightful-ux-training-app copied to clipboard
Delightful UX with React Native
Presentation: https://docs.google.com/presentation/d/1H3Se6jM-dOuYQQSCMGzfU3OzvHU8hBHowM-ELp7ZzKI/edit?usp=sharing
Running application
- 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
Exercises
Animations
Reanimated documentation: https://github.com/kmagiera/react-native-reanimated
Install Reanimated:
expo install react-native-reanimated
Heart icon animation (branch 1-favicon)
Create a function to animate FavouriteButton opacity smoothly.
In utils/animationHelpers:
- Create function returning
block-runLinearTiming. runLinearTimingshould 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. blockyou'll return should:- Check if clock is running (
cond,clockRunning), - If not yet, reset the clock (
set) and the state, updateconfig.toValueand start the clock (startClock), - Run
timingusingclock,stateandconfigwe 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
runLinearTimingtoopacityclass property. - Apply value we just animated to
opacitystyle inrender. - Remember to update manually
toValuewhen component updates (ComponentDidUpdate). - Remember to use
Animated.View!
Play/Pause button (branch 2-play-pause)
In Player:
- In body class create new
playingStateAnimated.Value. - In
handlePlayToggletoggleplayingStatevalue between 1 and 0. UsesetValue,condandeq. - Pass
playingStateprop to thePlayPauseButton.
In PlayPauseButton:
- Toggle
opacityof play and pause buttons. - Toggle
rotationof the container. - Use
runLinearTimingagain onpauseOpacity.toValueargument should equalisPlayingprop. - You can use
this.prop.isPlayingprop directly inrunLinearTiming- as it evaluates to1or0. - Use
interpolateto getplayOpacityandrotateY(from 0 to 180). - Use
concatnode to add 'deg' sufix to therotateY. - Use opacity and rotate values in
transformstyle of proper elements.
Progressbar ★
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 runLinearTimingfunction we created beforeValueandClock- of course
Collapsible header (branch 3-header)
In SongList:
- First create
AnimatedFlatListusingAnimated.createAnimatedComponentand put it in a place of regularFlatList- it will provide more props for us. - Uncomment
HeaderTitle. - Create
scrollYvalue before thereturn(Animated.Value) - Set
scrollEventThrottleprop of theAnimatedFlatListto 16 - this will provide smooth performance. - Use
AnimatedFlatList'sonScrollevent. - Extract
nativeEvent.contentOffset.yvalue from theeventand save it to thescrollY. - Pass
scrollYtoCollapsibleHeaderandHeaderTitlecomponents.
In CollapsibleHeader:
- Interpolate
translateY([0, 130] => [0, 130]),opacity([0, 200] => [1, 0]) andscale([0, 130] => [1, 0.6]) values basing onscrollYprop. - Add
Extrapolate.CLAMPto the interpolations - you don't want to exceed the range. - Change regular
ViewtoAnimated.Viewwhere necessary. - Use interpolated values in styles.
- In
imageContainerstyles apply translateY equal tothis.translateY* 0.3 (usemultiply). shadowContainershould have height equal tothis.translateY.
In HeaderTitle:
- Interpolate
titleOpacityclass field ([50, 100] => [0, 1]) basing onscrollYprop. - Add
Extrapolate.CLAMPto the interpolation. - Change regular
ViewtoAnimated.Viewwhere necessary. - Use interpolated value in styles.
Gesture handler
Documentation for RN Gesture Handler: https://kmagiera.github.io/react-native-gesture-handler/
Install Gesture Handler:
expo install react-native-gesture-handler
Make song item draggable (branch 4-drag-song-item)
Work in SongItem:
- Remember to change song
containerfromViewtoAnimated.View- you can't animate regular View, right? - Wrap
Animated.ViewusingPanGestureHandler. - Use
activeOffsetXprop to allow only swipe right. - Use
maxPointersprop to set number of fingers required for the gesture. - Create class constructor.
- In the constructor create
onGestureEventclass field. Similar way to flatlist scroll, assignAnimated.eventto it and extracttranslationXfrom theeventin the function. - Assign
this.onGestureEventtoonGestureEventandonHandlerStateChangeprop ofPanGestureHandler. - Use
translationXinAnimated.Valuestyle.
Revert translation when gesture ends (branch 5-revert-translation)
In SongItem:
- Create
const dragX- Animated Value - andthis.gestureStateAnimated Value equal toState.UNDETERMINEDin the constructor. - Create
const springClock-Animated.Clock- in the constructor. - Extract also
statefrom theevent. - Reassign
translationXfrom theevent- save it to our newdragXhelper. - Assign
condto 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
runSpringfunction you'll prepare in a moment. Call it withspringClockanddragXarguments.
In utils/animationHelpers:
- Create
runSpringfunction withclockandpositionarguments. - In the function body prepare
stateobject containingfinished,velocity,positionandtimevalues. - Prepare config object using
SpringUtils.makeDefaultConfig(). - Return following
block:- If
clockRunning(cond), reset state, restorestate.positionandstartClock. - Run
springwithclock,stateandconfigarguments. - If
state.finished(cond),stopClock. - Return
state.position.
- If
Hide the song if the gesture succeeded (branch 6-hide-song)
In SongItem:
- In the constructor create new
Animated.Valueequal to importedROW_HEIGHT. Let's call it justthis.height. - In the constructor create helper
const dragVelocityX- Animated Value. - Exctract
velocityXfrom theevent. - Create 2 new
clocksin the constructor:clockandswipeClock. - Stop those 2
clocksin thecondwe have for checking if the gesture isactive. - In the
condalready assigned to thetranslationXnest anothercond- check if gesture passed 80 breakpoint (greaterThan) (in a place of currentrunSpringcall). - If it didn't, it should revert as before (call
runSpringhere). - If succeeded, call block containing 2 functions:
runLinearTimingto animateSongItemheight to 0. Yourpositionargument will bethis.height.runSwipeDecayyou'll create in a moment. Call it withswipeClock,dragXanddragVelocityXarguments.
- Apply
this.heighttosongView style. - For nice effect, create
opacityAnimated Value in constuctor andinterpolateits value basing onthis.height. - Apply
this.opacityto the styles ofsong.
In utils/animationHelpers:
- Create
runSwipeDecayfunction. It should acceptclock,valueandvelocityarguments. - In the function body create :
stateobject containingfinished,velocity,positionandtimevaluesconfigobject containingdecelerationvalue equal to 0.99
- Return
blockcontaining:- Checking if
clockRunning; if false runblock, in which resetstateandconfigand runstartClock. - Calling
decaywithclock,stateandconfigarguments. - 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
runLinearTimingif the gesture passed the breakpoint; add another key to that function config:callbackwith 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
condin theblockyou return add callback call. Insert it to the sameblockasstopClock. - Call callback using
callnode with arguments[state.finished](must be an array; when any of the array values updates, the call will be triggered) andcallback.
Toggle active screen
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.
Theming (branch 8-theme-provider)
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
ThemeProviderandwithThemeusingcreateThemingmethod.
In App:
- Import
ThemeProviderand 2 themes. - Wrap main entry point using
ThemeProvider. - Pass
themeprop to theThemeProvider- this will help us to toggle theme in the app.
In Login:
- Import
withThemeHOC. - Wrap exported component with the HOC.
- Edit
stylesto method - it should consumethemeprop 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!
Internationalization
i18n-js documentation: https://github.com/fnando/i18n-js Localization documentation: https://docs.expo.io/versions/latest/sdk/localization/
Multi-language support (branch 9-internationalization)
Install both libraries:
yarn add i18n-js
expo install expo-localization
- Import
i18n-js,expo-localizationand translation objects (fromutils/translations). - Assign
Localization.localetoi18n.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!
RTL support
- Import
I18nManager. - Create condition under which the layout should be RTL (
isRTLvariable you can use later). - Set
I18nManager.allowRTLandI18nManager.forceRTL. - Test RTL layout and adjust styles if neccessary using
isRTL.
Accessibility (branch 10-accessibility)
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).
Android preparation
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
iOS preparation
On device go to:
- Settings
- General
- Accessibility
- Vision
- VoiceOver
Implementation
- Apply
accessibilityLabel,accessibilityHintwhere apropriate. - You can also use
i18n.t()to make it internationalized! - Hide labels next to
Togglecomponents usingaccessibilityElementsHiddenandimportantForAccessibility- to serve both platforms.
If you have some spare time, try to create one accessible element in a place of few visual ones!