# 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

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.