# Razvoj Mobilne Aplikacije korištenjem Expo U ovom projektu razvit ću mobilnu aplikaciju koja se sastoji od dva dijela: - Compass aplikacija - Level aplikacija ## Compass aplikacija Compass aplikacija koristi senzor magnetometra uređaja kako bi detektirala promjene u magnetskom polju, omogućujući određivanje orijentacije. Korisničko sučelje prikazuje kardinalne točke, trenutni stupanj rotacije te vizualnu reprezentaciju igle kompasa. <center> <img src="https://hackmd.io/_uploads/SkKxT4Udp.jpg" alt="compass-image" height='600'> </center> ## Level aplikacija Level aplikacija pruža digitalne funkcionalnosti libele ili inklinometra. Koristeći senzor akcelerometra uređaja, prati promjene u ubrzanju kako bi odredila nagib uređaja u x i y osima. Prikazuje vizualnu reprezentaciju mjehurića unutar kružne libele, pomažući korisnicima u procjeni horizontalne i vertikalne orijentacije njihovog uređaja. <center> <img src="https://hackmd.io/_uploads/HyHfp48u6.jpg" alt="level-image" height='600'> </center> ## Demo GIF aplikacije <center> <img src="https://hackmd.io/_uploads/Sy1VTVI_a.gif" alt="compass-gif" height='600'> </center> # Postavljanje radnog okruženja za Android Ako koristite Manjaro linux za postavljanje radnog okruženja trebat će vam: - Node - NativeScript CLI (sučelje naredbenog retka) - Android Studio - JDK (java razvojni komplet Android Studio nije strogo neophodan — no pruža sučelje jednostavno za korištenje za instaliranje i upravljanje Android SDK-ovima. ## Instalacija Node.js: ```cli sudo pacman -S nodejs ``` ## Instalacija OpenJDK: ```cli sudo pacman -S jdk-openjdk ``` ## Instalacija Android Studia: ```cli sudo pacman -S yay yay -S android-studio ``` Android Studio možete pokrenuti iz terminala naredbom: ```cli android-studio ``` Prilikom instalacija provjerite jesu li odabrane sljedeće komponente (popis bi se trebao pojaviti ako odaberete prilagođene opcije): - Android SDK - Android SDK platforma - Android virtualni uređaj Nakon instalacije konfigurirajte varijablu okruženja ANDROID_HOME za NativeScript kako bi mogao pronaći Android SDK i dodajte potrebne alate na put. ```cli export ANDROID_HOME=$HOME/Android/Sdk export PATH=$PATH:$ANDROID_HOME/platform-tools ``` ## Instalacija NativeScripta: ```cli npm install -g nativescript ``` ## Provjera okoline Kako biste provjerili je li instalacija bila uspješna, otvorite novi prozor naredbenog retka (kako biste bili sigurni da su nove varijable okruženja učitane) i pokrenite ```cli ns doktor android ``` Ako vidite da problemi nisu otkriveni, onda ste uspješno postavili svoj sustav. # Izrada Expo projekta Izradite račun na web stranici [Expo](https://expo.dev/), zatim pod izbornikom odaberite Projects i Create a Project ![create-expo-project](https://hackmd.io/_uploads/HyLk_SU_p.png) Projekt nazovite po želji te zatim stisnite na gumb Create. Nakon izrade dobit će te upute za razvoj i povezivanjem s lokalnim projektom. Povezivanje je moguće izvesti na dva načina: stvaranjem novog projekta ili povezivanje s postojećom kodnom bazom. <center> <img src="https://hackmd.io/_uploads/ByRSANv_6.png" alt="level-image" height='600'> </center> <br> Prema uputama za stvaranje novog projekta, otvorite terminal i unesite sljedeće naredbe: ```cli sudo npm install --global eas-cli npx create-expo-app compass cd compass eas init --id 50f02d03-d1c8-4fed-8f80-dadc7fe55506 ``` Nakon izrade projekta u terminalu dobivate upute za pokretanja projekta: ```cli ✅ Your project is ready! To run your project, navigate to the directory and run one of the following npm commands. - cd compass - npm run android - npm run ios # you need to use macOS to build the iOS project - use the Expo app if you need to do iOS development without a Mac - npm run web ``` Naredbom `cd compass` pozicionirajte se u direktorij projekta i zatim naredbom `code .` otvorite projekt u visual studio code-u. Unutar datoteke `App.js` nalazi se osnovna struktura React Native aplikacije iz koje se gradi cijela aplikacija: ```jsx // Uvoz neophodnih komponenti iz Expo-a i React Native-a import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Text, View } from 'react-native'; // Glavna komponenta aplikacije definirana kao funkcionalna komponenta export default function App() { return ( // Glavna View komponenta s stilom definiranim unutar 'container' stila u objektu stila {/* Text komponenta koja prikazuje poruku */} Otvorite App.js datoteku kako biste počeli raditi na svojoj aplikaciji! {/* Expo StatusBar komponenta za upravljanje statusnom trakom */} ); } // Stilovi za komponente definirani korištenjem metode StyleSheet.create const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', // Boja pozadine postavljena na bijelu alignItems: 'center', // Centriranje sadržaja horizontalno justifyContent: 'center', // Centriranje sadržaja vertikalno }, }); ``` Unutar projekta stvaram dvije datoteke `Compass.js` i `Level.js` i unutar foldera `assets` dodajem slike koje će biti upotrebljene pri izradi kompasa. Datoteku `App.js` ću izmijeniti tako da unutar nje stvorim navigaciju kojom ću se moći navigirati između datoteka `Compass.js` i `Level.js`. # Datoteka App.js U ovoj datoteci sadržana je osnovna konfiguracija navigacije koristeći React Navigation u React Native aplikaciji. Pomoću createStackNavigator definiraju se ekrani i njihovi međusobni odnosi. ## Import React i React Navigation komponenti: ```jsx import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import Level from './Level'; import Compass from './Compass'; ``` - **NavigationContainer:** Ovo je kontejner koji omotava cijelu navigacijsku hijerarhiju. - **createStackNavigator:** Funkcija koja stvara navigator za kreiranje "stoga" (stack-a) ekrana. ## Definiranje Stack Navigatora: ```jsx const Stack = createStackNavigator(); ``` - **Stack.Navigator:** Komponenta koja služi za grupiranje i definiranje ponašanja navigatora. ## Definiranje glavne aplikacije (App komponenta): ```jsx const App = () => { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Compass" screenOptions={{ headerShown: false, }} > <Stack.Screen name="Compass" component={Compass} /> <Stack.Screen name="Level" component={Level} /> </Stack.Navigator> </NavigationContainer> ); }; ``` - **NavigationContainer:** Omotava cijelu navigaciju i mora biti na vrhu hijerarhije. - **Stack.Navigator:** Definira stog ekrana i postavlja opcije za cijeli navigator. - **initialRouteName:** Postavlja početni ekran na "Compass". - **screenOptions:** Postavlja opcije za sve ekrane u navigatoru, ovdje se postavlja opcija headerShown na false kako bi se sakrio zaglavlje. ## Definiranje ekrana (Stack.Screen): ```jsx <Stack.Screen name="Compass" component={Compass} /> <Stack.Screen name="Level" component={Level} /> ``` - **Stack.Screen:** Definira pojedini ekran u navigatoru s atributima name (ime ekrana) i component (komponenta koja se renderira kada se prikaže taj ekran). ## Export glavne aplikacije: ```jsx export default App; ``` - **export default App:** Izvozi glavnu aplikaciju kako bi se mogla koristiti u drugim dijelovima projekta. # Datoteka Compass.js Ova datoteka predstavlja React Native komponentu Compass, koja koristi senzor magnetometra kako bi dobila orijentaciju uređaja. Komponenta prikazuje smjer na temelju očitanja magnetometra te pruža dodatne funkcije za upravljanje pretplatom na senzor. ## Funkcije ### Compass({ navigation }): ```jsx export default function Compass({ navigation }) { ``` - Glavna funkcionalna komponenta koja prikazuje smjer na temelju očitanja magnetometra. - Prima navigation prop za moguće navigacije između zaslona. ### useEffect(() => {...}, []): ```jsx useEffect(() => { _toggle(); return () => { _unsubscribe(); }; }, []); ``` - Hook koji se izvršava prilikom montiranja komponente. - Poziva _toggle funkciju za pokretanje senzora magnetometra. - Koristi se za inicijalnu konfiguraciju senzora i postavljanje čistača prilikom demontiranja komponente. ### _toggle(): ```jsx const _toggle = () => { if (subscription) { _unsubscribe(); } else { _subscribe(); } }; ``` - Funkcija koja prekida ili pokreće pretplatu na senzor magnetometra ovisno o trenutnom stanju pretplate (subscription). ### _subscribe(): ```jsx const _subscribe = () => { setSubscription( Magnetometer.addListener((data) => { setMagnetometer(_angle(data)); }) ); }; ``` - Funkcija koja pokreće pretplatu na senzor magnetometra. - Dodaje listener koji ažurira stanje magnetometer pozivom _angle funkcije. ### _unsubscribe(): ```jsx const _unsubscribe = () => { subscription && subscription.remove(); setSubscription(null); }; ``` - Funkcija koja prekida pretplatu na senzor magnetometra. - Uklanja postavljeni listener i postavlja subscription na null. ### _angle(magnetometer): ```jsx const _angle = (magnetometer) => { let angle = 0; if (magnetometer) { let { x, y, z } = magnetometer; if (Math.atan2(y, x) >= 0) { angle = Math.atan2(y, x) * (180 / Math.PI); } else { angle = (Math.atan2(y, x) + 2 * Math.PI) * (180 / Math.PI); } } return Math.round(angle); }; ``` - Funkcija koja izračunava kut na temelju očitanja senzora magnetometra. - Kut se dobiva pomoću arktangensa očitane vrijednosti senzora. ### _direction(degree): ```jsx const _direction = (degree) => { if (degree >= 22.5 && degree < 67.5) { return 'NE'; } else if (degree >= 67.5 && degree < 112.5) { return 'E'; } else if (degree >= 112.5 && degree < 157.5) { return 'SE'; } else if (degree >= 157.5 && degree < 202.5) { return 'S'; } else if (degree >= 202.5 && degree < 247.5) { return 'SW'; } else if (degree >= 247.5 && degree < 292.5) { return 'W'; } else if (degree >= 292.5 && degree < 337.5) { return 'NW'; } else { return 'N'; } }; ``` - Funkcija koja daje tekstualni smjer na temelju izračunatog kuta. - Koristi se za određivanje orijentacije uređaja prema osnovnim točkama kompasa (N, NE, E, SE, S, SW, W, NW). ### _degree(magnetometer): ```jsx const _degree = (magnetometer) => { return magnetometer - 90 >= 0 ? magnetometer - 90 : magnetometer + 271; }; ``` - Funkcija koja prilagođava očitavanje magnetometra kako bi se usklađivalo s prikazom 0° u smjeru pokazivača uređaja. - Pruža konzistentan prikaz smjera na temelju očitanja magnetometra. ## Povratni JSX Blok Komponente Compass Ovo je JSX blok koji čini sučelje komponente Compass. Koristi se kako bi se prikazao kompas s informacijama o orijentaciji uređaja, uključujući smjer, numerički prikaz kuta i vizualni prikaz kompasa. ### Grid: ```jsx <Grid style={styles.container}> ``` - Komponenta <Grid> predstavlja glavni kontejner koji koristi stilove definirane u styles.container. ### Navigacijski Gumbi: ```jsx <Row size={1}> <Col style={[styles.col1, styles.topButton]}> <Text style={[styles.buttonText, styles.boldText]}>Compass</Text> </Col> <Col style={[styles.col2, styles.topButton]}> <TouchableOpacity onPress={() => navigation.navigate('Level')}> <Text style={[styles.buttonText, styles.translucentText]}>Level</Text> </TouchableOpacity> </Col> </Row> ``` **<Row size={1}>** predstavlja prvi redak, a unutar njega su dvije kolone (<Col>). - Prva kolona (<Col>) sadrži tekst "Compass" s podebljanim stilom. - Druga kolona (<Col>) sadrži gumb "Level" koji poziva navigaciju na zaslon razine kada se pritisne. Ovaj gumb koristi TouchableOpacity za interaktivnost. ### Prikaz Smjera: ```jsx <Row style={{ alignItems: 'center', marginTop: -110 }} size={.9}> <Col style={{ alignItems: 'center' }}> <Text style={{ color: '#fff', fontSize: height / 26, fontWeight: 'bold' }}> {_direction(_degree(magnetometer))} </Text> </Col> </Row> ``` ### Pokazivač Komapasa : ```jsx <Row style={{ alignItems: 'center', marginTop: -60 }} size={.1}> <Col style={{ alignItems: 'center' }}> <View style={{ position: 'absolute', width: width, alignItems: 'center', top: 0 }}> <Image source={require('./assets/compass_pointer.png')} style={{ height: height / 26, resizeMode: 'contain' }} /> </View> </Col> </Row> ``` - Koristeći sliku compass_pointer.png. Slika se postavlja u apsolutni položaj kako bi se precizno pozicionirala. ### Prikaz Kuta: ```jsx <Row style={{ alignItems: 'center', marginTop: -30 }} size={2}> <Text style={{ color: '#fff', fontSize: height / 27, width: width, position: 'absolute', textAlign: 'center' }}> {_degree(magnetometer)}° </Text> <Col style={{ alignItems: 'center' }}> <Image source={require("./assets/compass_bg.png")} style={{ height: width - 80, justifyContent: 'center', alignItems: 'center', resizeMode: 'contain', transform: [{ rotate: 360 - magnetometer + 'deg' }] }} /> </Col> </Row> ``` - Ovaj redak kombinira tekstualni prikaz kuta i sliku kompasa. - Tekstualni prikaz kuta koristi funkciju _degree(magnetometer) za dinamičko dobivanje kuta. - Slika kompasa (compass_bg.png) postavljena je u središte i rotirana prema izračunatom kutu. ## Stilovi ```jsx const styles = StyleSheet.create({ ``` - **StyleSheet.create:** Funkcija koja stvara objekt stilova. Unutar objekta, svaki ključ predstavlja jedinstveni identifikator stila. ```jsx container: { backgroundColor: 'black', }, ``` - **container:** Stil za kontejner komponentu. ```jsx col1: { justifyContent: 'flex-start', alignItems: 'center', }, col2: { justifyContent: 'flex-start', alignItems: 'center', }, ``` - **col1 i col2:** Stilovi za kolone koje imaju flex-start poravnanje po vertikali i center po horizontali. ```jsx buttonText: { fontSize: 20, color: 'white', }, ``` - **buttonText:** Stil za tekst komponenti gumba. ```jsx topButton: { marginTop: 40, paddingTop: 10, paddingBottom: 0, }, ``` - **topButton:** Stil za gornji gumb s određenim marginama i razmakom. ```jsx translucentText: { opacity: 0.7, }, ``` - **translucentText:** Stil za tekst s polumprozirnom (70%) opačnosti. ```jsx boldText: { fontWeight: 'bold', }, ``` - **boldText:** Stil za tekst s podebljanim fontom. # Datoteka Level.js Komponenta `LevelScreen` je funkcionalna React komponenta koja koristi akcelerometar uređaja kako bi pratila promjene u nagibu po osima x i y. Prikazuje sirovu i izglađenu verziju podataka o nagibu te ih ažurira u stvarnom vremenu. ## Props ```jsx export default function LevelScreen({ navigation }) { ``` - **navigation:** Prop koji omogućuje navigaciju između ekrana unutar aplikacije. Detalji o navigaciji nisu navedeni u ovom isječku koda. ## Stanja ### Stanje podataka o akcelerometru ([sensorData, setSensorData]): ```jsx const [sensorData, setSensorData] = useState({ x: 0, y: 0, z: 0, }); ``` - **x:** Ubrzanje duž x-osi. - **y:** Ubrzanje duž y-osi. - **z:** Ubrzanje duž z-osi (nije korišteno u ovom slučaju). - **setSensorData:** Funkcija za ažuriranje stanja s novim podacima akcelerometra. ### Stanje izglađenih podataka ([smoothedData, setSmoothedData]): ```jsx const [smoothedData, setSmoothedData] = useState({ x: 0, y: 0, }); ``` - **x:** Izglađeno ubrzanje duž x-osi. - **y:** Izglađeno ubrzanje duž y-osi. - **setSmoothedData:** Funkcija za ažuriranje stanja s novim izglađenim podacima. ### Stanje pretplate ([subscription, setSubscription]): ```jsx const [subscription, setSubscription] = useState(null); ``` - **subscription:** Predstavlja objekt pretplate koji vraća događajni slušač akcelerometra. - **setSubscription:** Funkcija za ažuriranje stanja pretplate. ## Funkcije ### Funkcija smoothData (smoothData(newData)): ```jsx const smoothData = (newData) => { setSmoothedData((prevSmoothedData) => ({ x: prevSmoothedData.x + SMOOTHING_FACTOR * (newData.x - prevSmoothedData.x), y: prevSmoothedData.y + SMOOTHING_FACTOR * (newData.y - prevSmoothedData.y), })); }; ``` - Izglađuje nove podatke (newData) pomoću eksponencijalnog izglađivanja. - Koristi faktor izglađivanja (SMOOTHING_FACTOR) za postizanje željenog stupnja glatkosti. - Ažurira stanje izglađenih podataka. ### Funkcija _subscribe (_subscribe()): ```jsx const _subscribe = () => { setSubscription(Accelerometer.addListener(({ x, y }) => { const newData = { x, y }; setSensorData(newData); smoothData(newData); })); }; ``` - Postavlja događajnog slušača akcelerometra za praćenje promjena u ubrzanju. - Ažurira stanje sirovih podataka akcelerometra i primjenjuje izglađivanje pozivom funkcije smoothData. ### Funkcija _unsubscribe (_unsubscribe()): ```jsx const _unsubscribe = () => { subscription && subscription.remove(); setSubscription(null); }; ``` - Uklanja pretplatu događajnog slušača ako postoji. - Postavlja stanje pretplate na null. ### Nuspojave: ```jsx useEffect(() => { _subscribe(); return () => _unsubscribe(); }, []); ``` - **useEffect** kuka se koristi kako bi se _subscribe funkcija pozvala prilikom montiranja komponente te _unsubscribe funkcija prilikom odmontiranja. - Efekt se izvršava samo jednom, zahvaljujući praznom nizu ovisnosti ([]), osiguravajući da se ne ponavlja pri svakom naknadnom renderiranju. ### Transformacije ```jsx const circle3Style = { transform: [ { translateX: smoothedData.x * 135 }, { translateY: -smoothedData.y * 135 }, ], }; ``` **translateX: smoothedData.x * 135 (Pomak na X-osi):** - smoothedData.x predstavlja izglađeno ubrzanje duž x-osi. - Množenje s faktorom 135 prilagođava pomak tako da je proporcionalan izglađenom ubrzanju. - Pozitivna vrijednost pomaknut će komponentu udesno, a negativna vrijednost ulijevo. **translateY: -smoothedData.y * 135 (Pomak na Y-osi):** - smoothedData.y predstavlja izglađeno ubrzanje duž y-osi. - Množenje s faktorom 135 prilagođava pomak tako da je proporcionalan izglađenom ubrzanju. - Pozitivna vrijednost pomaknut će komponentu prema dolje, a negativna vrijednost prema gore. ## Struktura JSX Bloka Ova komponenta je odgovorna za prikazivanje ekrana unutar aplikacije. Struktura uključuje gumbiće za navigaciju između ekrana (Compass i Level) te prikazuje tri kruga smještena unutar circleContainer. ### Grid Komponenta: ```jsx <Grid style={styles.container}> ``` - Kontejner koji koristi stil styles.container. - Stil definira crnu pozadinu ekrana. ### Prvi Redak: ```jsx <Row size={1}> <Col style={[styles.col1, styles.topButton]}> <TouchableOpacity onPress={() => navigation.navigate('Compass')}> <Text style={[styles.buttonText, styles.translucentText]}>Compass</Text> </TouchableOpacity> </Col> <Col style={[styles.col2, styles.topButton]}> <Text style={[styles.buttonText, styles.boldText]}>Level</Text> </Col> </Row> ``` - Sadrži dva stupca (Col komponente). - Prvi stupac (gumb "Compass") je interaktivan i koristi navigacijsku funkciju za prijelaz na ekran "Compass". - Drugi stupac (gumb "Level") je statičan. ### Drugi Redak: ```jsx <Row size={2}> <Col> <View style={styles.circleContainer}> <View style={styles.circle1}></View> <View style={[styles.circle3, circle3Style]}></View> <View style={styles.circle2}></View> </View> </Col> </Row> ``` - Sadrži jedan stupac (Col komponentu). - Stupac sadrži View komponentu (circleContainer) koja grupira tri kruga (circle1, circle3, circle2). ## Stilovi ### container: ```jsx container: { flex: 1, backgroundColor: 'black', justifyContent: 'flex-start', alignItems: 'flex-start', marginTop: 0, }, ``` - **flex: 1:** Prostiranje komponente prema svim dostupnim prostornim dimenzijama. - **backgroundColor: 'black':** Postavljanje pozadinske boje na crnu. - **justifyContent: 'flex-start':** Poravnanje djece prema vrhu kontejnera. - **alignItems: 'flex-start':** Poravnanje djece prema lijevom rubu ### col1 i col2: ```jsx col1: { justifyContent: 'flex-start', alignItems: 'center', }, col2: { justifyContent: 'flex-start', alignItems: 'center', }, ``` - **justifyContent: 'flex-start':** Poravnanje sadržaja prema vrhu kolona. - **alignItems: 'center':** Centralno poravnanje elemenata unutar kolona. ### buttonText: ```jsx buttonText: { fontSize: 20, color: 'white', }, ``` - **fontSize: 20:** Postavljanje veličine fonta na 20. - **color: 'white':** Postavljanje boje teksta na bijelu. ### topButton: ```jsx topButton: { marginTop: 40, padding: 10, }, ``` - **marginTop: 40:** Postavljanje vršnog razmaka na 40. - **padding: 10:** Postavljanje unutarnjeg razmaka gumbića. ### translucentText: ```jsx translucentText: { opacity: 0.7, }, ``` - **opacity: 0.7:** Postavljanje prozirnosti teksta na 0.7, čime se postiže efekt poluprovidnosti. ### boldText: ```jsx boldText: { fontWeight: 'bold', }, ``` - **fontWeight: 'bold':** Postavljanje debljine teksta na bold (deblje). ### circleContainer: ```jsx circleContainer: { flexDirection: 'row', justifyContent: 'center', }, ``` - **flexDirection: 'row':** Postavljanje smjera osi na vodoravno, tako da se krugovi prikazuju u istom retku. - **justifyContent: 'center':** Centralno poravnanje krugova unutar retka. ### circle1: ```jsx circle1: { borderColor: 'white', borderWidth: 0.6, height: 300, width: 300, borderRadius: 150, opacity: 0.7, position: 'relative', }, ``` - **borderColor: 'white':** Postavljanje boje ruba kruga na bijelu. - **borderWidth: 0.6:** Postavljanje debljine ruba na 0.6. - **height: 300, width: 300:** Postavljanje visine i širine kruga na 300. - **borderRadius: 150:** Postavljanje radijusa kruga na polovicu visine/širine kako bi se dobio krug. - **opacity: 0.7:** Postavljanje prozirnosti kruga na 0.7. - **position: 'relative':** Postavljanje relativnog pozicioniranja. ### circle2: ```jsx circle2: { borderColor: 'white', borderWidth: 2, height: 15, width: 15, borderRadius: 10, position: 'absolute', top: '50%', left: '50%', marginLeft: -7.5, marginTop: -7.5, }, ``` - **borderColor: 'white':** Boja ruba kruga. - **borderWidth: 2:** Debljina ruba. - **height: 15, width: 15:** Dimenzije kruga. - **borderRadius: 10:** Radijus kruga (polovina visine/širine). - **position: 'absolute':** Apsolutno pozicioniranje. - **top: '50%', left: '50%':** Postavljanje na sredinu roditeljskog elementa. - **marginLeft: -7.5, marginTop: -7.5:** Korekcija pozicije kako bi se krug centrirao. ### circle3: ```jsx circle3: { borderColor: 'red', borderWidth: 3, height: 30, width: 30, borderRadius: 15, opacity: 0.75, position: 'absolute', top: '50%', left: '50%', marginLeft: -15, marginTop: -15, }, ``` - **borderColor: 'red':** Boja ruba kruga. - **borderWidth: 3:** Debljina ruba. - **height: 30, width: 30:** Dimenzije kruga. - **borderRadius: 15:** Radijus kruga (polovina visine/širine). - **opacity: 0.75:** Prozirnost kruga. - **position: 'absolute':** Apsolutno pozicion # Cijeli kod aplikacije ## App.js ```jsx= import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import Level from './Level'; import Compass from './Compass'; const Stack = createStackNavigator(); const App = () => { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Compass" screenOptions={{ headerShown: false, }} > <Stack.Screen name="Compass" component={Compass} /> <Stack.Screen name="Level" component={Level} /> </Stack.Navigator> </NavigationContainer> ); }; export default App; ``` ## Compass.js ```jsx= import React, { useState, useEffect } from 'react'; import { StyleSheet, Text, TouchableOpacity, View, Image, Dimensions } from 'react-native'; import { Magnetometer } from 'expo-sensors'; import { Grid, Col, Row } from 'react-native-easy-grid'; const { height, width } = Dimensions.get('window'); export default function Compass({ navigation }) { const [subscription, setSubscription] = useState(null); const [magnetometer, setMagnetometer] = useState(0); useEffect(() => { _toggle(); return () => { _unsubscribe(); }; }, []); const _toggle = () => { if (subscription) { _unsubscribe(); } else { _subscribe(); } }; const _subscribe = () => { setSubscription( Magnetometer.addListener((data) => { setMagnetometer(_angle(data)); }) ); }; const _unsubscribe = () => { subscription && subscription.remove(); setSubscription(null); }; const _angle = (magnetometer) => { let angle = 0; if (magnetometer) { let { x, y, z } = magnetometer; if (Math.atan2(y, x) >= 0) { angle = Math.atan2(y, x) * (180 / Math.PI); } else { angle = (Math.atan2(y, x) + 2 * Math.PI) * (180 / Math.PI); } } return Math.round(angle); }; const _direction = (degree) => { if (degree >= 22.5 && degree < 67.5) { return 'NE'; } else if (degree >= 67.5 && degree < 112.5) { return 'E'; } else if (degree >= 112.5 && degree < 157.5) { return 'SE'; } else if (degree >= 157.5 && degree < 202.5) { return 'S'; } else if (degree >= 202.5 && degree < 247.5) { return 'SW'; } else if (degree >= 247.5 && degree < 292.5) { return 'W'; } else if (degree >= 292.5 && degree < 337.5) { return 'NW'; } else { return 'N'; } }; const _degree = (magnetometer) => { return magnetometer - 90 >= 0 ? magnetometer - 90 : magnetometer + 271; }; return ( <Grid style={styles.container}> <Row size={1}> <Col style={[styles.col1, styles.topButton]}> <Text style={[styles.buttonText, styles.boldText]}>Compass</Text> </Col> <Col style={[styles.col2, styles.topButton]}> <TouchableOpacity onPress={() => navigation.navigate('Level')}> <Text style={[styles.buttonText, styles.translucentText]}>Level</Text> </TouchableOpacity> </Col> </Row> <Row style={{ alignItems: 'center', marginTop: -110 }} size={.9}> <Col style={{ alignItems: 'center' }}> <Text style={{ color: '#fff', fontSize: height / 26, fontWeight: 'bold' }}> {_direction(_degree(magnetometer))} </Text> </Col> </Row> <Row style={{ alignItems: 'center', marginTop: -60 }} size={.1}> <Col style={{ alignItems: 'center' }}> <View style={{ position: 'absolute', width: width, alignItems: 'center', top: 0 }}> <Image source={require('./assets/compass_pointer.png')} style={{ height: height / 26, resizeMode: 'contain' }} /> </View> </Col> </Row> <Row style={{ alignItems: 'center', marginTop: -30 }} size={2}> <Text style={{ color: '#fff', fontSize: height / 27, width: width, position: 'absolute', textAlign: 'center' }}> {_degree(magnetometer)}° </Text> <Col style={{ alignItems: 'center' }}> <Image source={require("./assets/compass_bg.png")} style={{ height: width - 80, justifyContent: 'center', alignItems: 'center', resizeMode: 'contain', transform: [{ rotate: 360 - magnetometer + 'deg' }] }} /> </Col> </Row> <Row style={{ alignItems: 'center' }} size={1}> <Col style={{ alignItems: 'center' }}> </Col> </Row> </Grid> ); } const styles = StyleSheet.create({ container: { backgroundColor: 'black', }, col1: { justifyContent: 'flex-start', alignItems: 'center', }, col2: { justifyContent: 'flex-start', alignItems: 'center', }, buttonText: { fontSize: 20, color: 'white', }, topButton: { marginTop: 40, paddingTop: 10, paddingBottom: 0, }, translucentText: { opacity: 0.7, }, boldText: { fontWeight: 'bold', }, }); ``` ## Level.js ```jsx= import React, { useState, useEffect } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Grid, Row, Col } from 'react-native-easy-grid'; import { Accelerometer } from 'expo-sensors'; const SMOOTHING_FACTOR = 0.2; // Adjust this value to control the smoothing level export default function LevelScreen({ navigation }) { const [sensorData, setSensorData] = useState({ x: 0, y: 0, z: 0, }); const [smoothedData, setSmoothedData] = useState({ x: 0, y: 0, }); const [subscription, setSubscription] = useState(null); const smoothData = (newData) => { setSmoothedData((prevSmoothedData) => ({ x: prevSmoothedData.x + SMOOTHING_FACTOR * (newData.x - prevSmoothedData.x), y: prevSmoothedData.y + SMOOTHING_FACTOR * (newData.y - prevSmoothedData.y), })); }; const _subscribe = () => { setSubscription(Accelerometer.addListener(({ x, y }) => { const newData = { x, y }; setSensorData(newData); smoothData(newData); })); }; const _unsubscribe = () => { subscription && subscription.remove(); setSubscription(null); }; useEffect(() => { _subscribe(); return () => _unsubscribe(); }, []); const circle3Style = { transform: [ { translateX: smoothedData.x * 135 }, { translateY: -smoothedData.y * 135 }, ], }; return ( <Grid style={styles.container}> <Row size={1}> <Col style={[styles.col1, styles.topButton]}> <TouchableOpacity onPress={() => navigation.navigate('Compass')}> <Text style={[styles.buttonText, styles.translucentText]}>Compass</Text> </TouchableOpacity> </Col> <Col style={[styles.col2, styles.topButton]}> <Text style={[styles.buttonText, styles.boldText]}>Level</Text> </Col> </Row> <Row size={2}> <Col> <View style={styles.circleContainer}> <View style={styles.circle1}></View> <View style={[styles.circle3, circle3Style]}></View> <View style={styles.circle2}></View> </View> </Col> </Row> </Grid> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'black', justifyContent: 'flex-start', alignItems: 'flex-start', marginTop: 0, }, col1: { justifyContent: 'flex-start', alignItems: 'center', }, col2: { justifyContent: 'flex-start', alignItems: 'center', }, buttonText: { fontSize: 20, color: 'white', }, topButton: { marginTop: 40, padding: 10, }, translucentText: { opacity: 0.7, }, boldText: { fontWeight: 'bold', }, circleContainer: { flexDirection: 'row', justifyContent: 'center', }, circle1: { borderColor: 'white', borderWidth: 0.6, height: 300, width: 300, borderRadius: 150, opacity: 0.7, position: 'relative', }, circle2: { borderColor: 'white', borderWidth: 2, height: 15, width: 15, borderRadius: 10, position: 'absolute', top: '50%', left: '50%', marginLeft: -7.5, marginTop: -7.5, }, circle3: { borderColor: 'red', borderWidth: 3, height: 30, width: 30, borderRadius: 15, opacity: 0.75, position: 'absolute', top: '50%', left: '50%', marginLeft: -15, marginTop: -15, }, }); ``` # Pokretanje aplikacije Pozicionirajte se u direktorij projekta i pokrenite naredbu: ```cli npm run android ``` - Ova naredba će pokrenuti Expo projekt na Android uređaju ili emulatoru, ovisno o postavkama vašeg sustava. - Expo CLI će automatski prepoznati i pokrenuti aplikaciju na Android uređaju ako je spojen ili na emulatoru ako je dostupan ```cli $ npm run android  ✔ > compass@1.0.0 android > expo start --android Starting project at /home/mbakk/compass (node:38612) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. (Use `node --trace-deprecation ...` to show where the warning was created) Starting Metro Bundler › Opening emulator Pixel_3a_API_34_extension_level_7_x86_64 › Opening exp://192.168.1.34:8081 on Pixel_3a_API_34_extension_level_7_x86_64 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ █ ▄▄▄▄▄ █ █▄ █▄▀█ ▄▄▄▄▄ █ █ █ █ █ ▀▄ █▀█▀▄█ █ █ █ █ █▄▄▄█ █▀██▀▀█▀▄██ █▄▄▄█ █ █▄▄▄▄▄▄▄█▄▀▄█ █▄█▄█▄▄▄▄▄▄▄█ █ ▀▀ ▄▄█▀▀▀▄▀█▄ ███ ▀▄▄ ▄█ ███ ▀▀▀▄ ▀█▀ ▄██ ▀▀ █▄ ▀██ █ ▀▄█ ▄███▄█▄▀▄▀▄▀▄▀▀▄ ▀██ ███ █ ▄▀▄▄▄█▀██▄▄▄█▄▀ ▀███ █▄▄██▄▄▄▄▀▀▀▄▀█▄▄ ▄▄▄ ▀ ▄▄█ █ ▄▄▄▄▄ █▀█▄ ▄██▀ █▄█ ▀▀█▀█ █ █ █ █▄▄▀█▄▀▄█▄▄ ▄▄▀ █ █ █▄▄▄█ █▀█▀█▀█▀▄██▄▀█▀▀ ██ █▄▄▄▄▄▄▄█▄▄█▄▄▄▄████▄▄▄▄▄▄█ › Metro waiting on exp://192.168.1.34:8081 › Scan the QR code above with Expo Go (Android) or the Camera app (iOS) › Using Expo Go › Press s │ switch to development build › Press a │ open Android › Press w │ open web › Press j │ open debugger › Press r │ reload app › Press m │ toggle menu › Press o │ open project code in your editor › Press ? │ show all commands Logs for your project will appear below. Press Ctrl+C to exit. ``` ## Pregled na stvarnom uređaju: Ako želite pokrenuti aplikaciju na vlastitom Android uređaju preuzmite i instalirajte **`Expo Go`** aplikaciju s Google Play trgovine. Otvorite Expo Go aplikaciju i skenirajte generirani QR kod pomoću kamere aplikacije Expo Go.