В очередном выпуске «Как создать андроид-приложение» мы рассмотрим приложение с эффектом дополненной реальности, как игра PokemonGo. Да, да, мы тоже будем с вами ловить покемонов, привязанных к определенным координатам на местности, используя датчики android устройства.
Итак, что такое «Дополненная реальность»? Этот термин стал довольно популярным в последние несколько лет благодаря Google glass, но идея старше, чем первый телефон Android. Вы помните фильм Терминатор? Наш герой имел зрение, которое отображало расстояние до ближайших объектов и дополнительную информацию о них.
Существует несколько определений дополненной реальности: исследователь Рональд Азума (англ. Ronald Azuma) в 1997 году определил её как систему, которая:
- совмещает виртуальное и реальное;
- взаимодействует в реальном времени;
- работает в 3D.
Не путайте дополненную реальность с виртуальной реальностью — это разные технологии. Отличие заключается в том, что дополненная реальность — это наложение оцифрованной информации на реальный мир. Давайте рассмотрим простое приложение, которое отображает привязанную к координатам картинку на CameraView. Картинка отображается поверх изображения с камеры, если устройство находится поблизости и повернуто в сторону координат расположения картинки. С координатами местоположения понятно, но каким образом устройство будет определять направление расположения устройства относительно точки привязки картинки? В этом нам поможет теория геодезии и такое понятие, как азимут.
Немного теории
Кто не знает, азимут — это угол, образуемый заданным направлением движения и направлением на север.

Приложение будет определять точку назначения, сравнивая азимут, вычисленный из основных свойств прямоугольного треугольника и реальный азимут, который указывает устройство. Вот примерный алгоритм для достижения этой цели:
- получить местоположение устройства
- получить местоположение точки назначения
- вычислить расстояние до точки назначения
- рассчитать теоретический азимут
- получить реальный азимут устройства
- сравнить оба азимута
- вызвать событие при совпадении значений азимута и расстояния в пределах допустимой погрешности
Теперь вопрос заключается в том, как вычислить азимут и расстояние. Это довольно просто, потому что мы будем игнорировать кривизну Земли и рассматривать ее как плоскую поверхность:

Как вы можете видеть, у нас есть прямоугольный треугольник, и мы можем вычислить угол ᵠ между точками с помощью простого уравнения:
В приведенной таблице представлены отношения между уголом в градах и азимутом A(AB):

А чтобы определить расстояние до точки назначения, воспользуемся этой формулой:
Рассмотрим реализацию нашего приложения
Ниже код проекта в Андроид Студио, который мы сейчас подробно разберем.
Для начала посмотрим ресурсы. Нам понадобится изображение покемона.
Вот ссылка на ресурсы: https://yadi.sk/d/Y06QyQ6juJibC
В нашем приложении 2 активити, поэтому нам нужны 2 макета компоновки для них. В одном будет отображаться превью камеры с наложением текста и картинки, а также кнопка для перехода на карту.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <SurfaceView android:id="@+id/cameraview" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <ImageView android:id="@+id/icon" android:layout_width="96dp" android:layout_height="96dp" android:src="@drawable/pikachu" android:visibility="invisible" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> <TextView android:id="@+id/cameraTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large Text" android:layout_alignParentTop="true" android:layout_alignParentStart="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Map" android:id="@+id/btnMap" android:layout_marginBottom="23dp" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" /> <ImageView android:layout_width="224dp" android:layout_height="224dp" android:id="@+id/imageView" android:src="@drawable/reticle" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> </RelativeLayout> |
В другом макете будет отображаться карта Google Maps.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="info.fandroid.example.augmentedreality.MapActivity"> <fragment android:id="@+id/mapView" android:name="com.google.android.gms.maps.MapFragment" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout> |
Я не буду подробно останавливаться на том, как встроить карту Google Maps в приложение, урок об этом смотрите на нашем сайте: ссылка
Теперь перейдем к коду. Нам понадобятся два интерфейса. Первый — это слушатель изменения местоположения.
1 2 3 4 5 |
import android.location.Location; public interface OnLocationChangedListener { void onLocationChanged(Location currentLocation); } |
Второй — слушатель изменения азимута.
1 2 3 |
public interface OnAzimuthChangedListener { void onAzimuthChanged( float azimuthFrom, float azimuthTo); } |
В классе MyCurrentLocation будем определять местоположение устройства. Этот класс реализует такие интерфейсы: GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
import android.content.Context; import android.location.Location; import android.os.Bundle; import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; public class MyCurrentLocation implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { private GoogleApiClient mGoogleApiClient; private Location mLastLocation; private LocationRequest mLocationRequest; private OnLocationChangedListener onLocationChangedListener; //передаем интерфейс OnLocationChangedListener в конструкторе для организации //прослушивания события смены местоположения public MyCurrentLocation(OnLocationChangedListener onLocationChangedListener) { this. onLocationChangedListener = onLocationChangedListener; } /** * Создает GoogleApiClient. Использует метод { @code #addApi} для запроса * LocationServices API. */ protected synchronized void buildGoogleApiClient(Context context) { mGoogleApiClient = new GoogleApiClient.Builder(context) .addConnectionCallbacks( this) .addOnConnectionFailedListener( this ) .addApi(LocationServices. API) .build(); //создаем запрос и устанавливаем интервал для его отправки mLocationRequest = LocationRequest. create() .setPriority(LocationRequest. PRIORITY_HIGH_ACCURACY ) .setInterval( 10 * 1000) // 10 seconds, in milliseconds .setFastestInterval(1 * 1000 ); // 1 second, in milliseconds } public void start(){ //Подключает клиента к службам Google Play. mGoogleApiClient.connect(); } public void stop(){ //Закрывает подключение к службам Google Play. mGoogleApiClient.disconnect(); } //После вызова connect(), этот метод будет вызываться асинхронно после успешного завершения запроса подключения. @Override public void onConnected(Bundle bundle) { LocationServices.FusedLocationApi .requestLocationUpdates( mGoogleApiClient, mLocationRequest , this ); mLastLocation = LocationServices.FusedLocationApi .getLastLocation( mGoogleApiClient); if ( mLastLocation != null ) { onLocationChangedListener.onLocationChanged( mLastLocation ); } } //Вызывается, когда клиент временно в отключенном состоянии. @Override public void onConnectionSuspended( int i) { } //Вызывается, когда произошла ошибка при подключении клиента к службе. @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.e( "MyApp" , "Location services connection failed with code " + connectionResult.getErrorCode()); } /* * Реализуем метод onLocationChanged интерфейса LocationListener. Обратный вызов, который возникает, когда изменяется местоположение. * Здесь создаем объект mLastLocation, который хранит последнее местоположение и передаем его в методе интерфейса. */ @Override public void onLocationChanged(Location location) { mLastLocation = LocationServices.FusedLocationApi .getLastLocation( mGoogleApiClient); if ( mLastLocation != null ) { onLocationChangedListener.onLocationChanged( mLastLocation ); } } } |
Это местоположение мы будем получать через конструктор этого класса в главном классе приложения, который будет реализовать интерфейс nLocationChangedListener.
А теперь смотрим класс для определения азимута по методу, о котором я говорил в начале урока. Класс MyCurrentAzimuth имплементирует интерфейс SensorEventListener.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; public class MyCurrentAzimuth implements SensorEventListener { private SensorManager sensorManager; private Sensor sensor; private int azimuthFrom = 0 ; private int azimuthTo = 0 ; private OnAzimuthChangedListener mAzimuthListener; Context mContext ; //в конструкторе передаем интерфейс OnAzimuthChangedListener и контекст public MyCurrentAzimuth(OnAzimuthChangedListener azimuthListener, Context context) { mAzimuthListener = azimuthListener; mContext = context; } //подключаемся к сенсору и регистрируем слушатель для данного датчика с заданной периодичностью //SENSOR_DELAY_UI - частота обновления пользовательского интерфейса. //TYPE_ROTATION_VECTOR - Возвращает положение устройства в пространстве в виде угла //относительно оси Z, указывающей на север. // Виртуальный датчик, берущий показания от акселерометра, гироскопа и датчика магнитного поля. public void start(){ sensorManager = (SensorManager) mContext .getSystemService( mContext. SENSOR_SERVICE); sensor = sensorManager .getDefaultSensor(Sensor. TYPE_ROTATION_VECTOR); sensorManager.registerListener( this, sensor , SensorManager. SENSOR_DELAY_UI); } //Отменяет регистрацию слушателя для всех датчиков. public void stop(){ sensorManager.unregisterListener( this ); } //вызывается при новом событии датчика //получаем матрицу вращения устройства // в переменную azimuthTo сохраняем градусную меру угла поворота в радианах @Override public void onSensorChanged(SensorEvent event) { azimuthFrom = azimuthTo; float[] orientation = new float[ 3]; float[] rMat = new float[ 9]; SensorManager.getRotationMatrixFromVector (rMat, event. values); azimuthTo = ( int) ( Math. toDegrees( SensorManager.getOrientation( rMat, orientation )[ 0] ) + 360 ) % 360 ; mAzimuthListener.onAzimuthChanged( azimuthFrom , azimuthTo ); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } } |
Как видите, здесь используется виртуальный датчик, TYPE_ROTATION_VECTOR, который берет информацию сразу с нескольких сенсоров — акселерометра, гироскопа и датчика магнитного поля. И если акселерометр есть в каждом устройстве, то датчик магнитного поля в старых бюджетных телефонах зачастую отсутствует. Следовательно, на таком смартфоне азимут определяться не будет, так как датчик TYPE_ROTATION_VECTOR будет всегда возвращать 0. Но позже я покажу, как мы обойдем это ограничение, чтобы наше приложение работало.
А как получить список сенсоров в вашем устройстве — смотрите по ссылке на экране и в описании видео. На нашем сайте есть небольшая инструкция об этом с перечнем и назначением основных сенсоров: ссылка
А мы рассмотрим следующий класс — MapActivity. Его задача — отображение карты с меткой в месте привязки покемона.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.Toast; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.MapFragment; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; public class MapActivity extends AppCompatActivity { GoogleMap googleMap ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout. activity_map); createMapView(); addMarker(); setTitle(getString(R.string. p_name)); } private void createMapView(){ try { if( null == googleMap ){ googleMap = ((MapFragment) getFragmentManager().findFragmentById( R.id. mapView )).getMap(); if( null == googleMap ) { Toast. makeText(getApplicationContext(), "Error creating map",Toast. LENGTH_SHORT).show(); } } } catch (NullPointerException exception){ Log.e( "mapApp" , exception.toString()); } } private void addMarker(){ double lat = CameraViewActivity.TARGET_LATITUDE; double lng = CameraViewActivity.TARGET_LONGITUDE; CameraPosition cameraPosition = new CameraPosition.Builder() .target( new LatLng(lat, lng)) .zoom( 15) .build(); CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition); googleMap.animateCamera(cameraUpdate); if( null != googleMap ){ googleMap.addMarker( new MarkerOptions() .position( new LatLng(lat, lng)) .title(getString(R.string. p_name )) .draggable( false ) ); } } } |
Здесь мы просто создаем карту Google Map и добавляем маркер в точку с заранее определенными координатами, для простоты здесь мы их берем из переменных в классе главного активити. В методе добавления маркера также устанавливаем позицию камеры и масштаб карты. Для работы с Google Maps нужно получить apikey в консоли разработчика Google и прописать его в манифесте приложения. Подробнее о получении ключа и работе с картами Google смотрите тут и тут.
Еще нам понадобится класс Pikachu — здесь просто набор переменных: имя и координаты, а также геттеры для них.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Pikachu { private String mName; private double mLatitude ; private double mLongitude ; public Pikachu(String newName, double newLatitude, double newLongitude) { this. mName = newName; this. mLatitude = newLatitude; this. mLongitude = newLongitude; } public String getPoiName() { return mName; } public double getPoiLatitude() { return mLatitude; } public double getPoiLongitude() { return mLongitude; } } |
После того как вы подготовили данные от датчиков, пришло время для реализации главного класса CameraViewActivity. Первая и наиболее важная вещь заключается в реализации инетрфейса SurfaceHolder.Callback который позволяет отправить изображение с камеры в наш макет и обработать связанные с этим различные события. Интерфейс реализует три метода, ответственных за это: surfaceChanged() surfaceCreated() surfaceDestroyed(). Позже мы рассмотрим реализыцию этих методов более подробно.
Класс CameraViewActivity имплементирует также интерфейсы OnLocationChangedListener, OnAzimuthChangedListener, с их методами onLocationChanged и onAzimuthChanged, а также интерфейс View.OnClickListener — слушатель нажатия кнопки с методом onClick.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; import android.hardware.Camera; import android.location.Location; import android.os.Bundle; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class CameraViewActivity extends Activity implements SurfaceHolder.Callback, OnLocationChangedListener, OnAzimuthChangedListener, View.OnClickListener { //объявляем необходимые переменные private Camera mCamera; private SurfaceHolder mSurfaceHolder; private boolean isCameraviewOn = false ; private Pikachu mPoi; private double mAzimuthReal = 0 ; private double mAzimuthTeoretical = 0 ; /*нам понадобятся, помимо прочего, две константы для хранения допустимых отклонений дистанции и азимута устройства от целевых. Значения подобраны практически, вы можете их менять, чтобы облегчить, или наоборот, усложнить задачу поиска покемона. Точность дистанции указана в условных единицах, равных примерно 0.9м, а точность азимута - в градусах*/ private static final double DISTANCE_ACCURACY = 20 ; private static final double AZIMUTH_ACCURACY = 10 ; private double mMyLatitude = 0 ; private double mMyLongitude = 0 ; /*также создаем константы с координатами цели, это будет местоположение покемона. Здесь укажите широту и долготу любого места, которое находится недалеко от вас - например, координаты соседнего двора или ближайшего магазина - чтобы далеко не бегать. Особо ленивые могут указать свое текущее местоположение. Получить координаты любого места можно, например, через приложение Google карты. */ public static final double TARGET_LATITUDE = 27.590377 ; public static final double TARGET_LONGITUDE = 14.425153 ; private MyCurrentAzimuth myCurrentAzimuth; private MyCurrentLocation myCurrentLocation; TextView descriptionTextView; ImageView pointerIcon; Button btnMap; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout. activity_camera_view); setRequestedOrientation(ActivityInfo. SCREEN_ORIENTATION_PORTRAIT); setupListeners(); setupLayout(); setAugmentedRealityPoint(); } //создаем экземпляр покемона с указанием координат его местоположения private void setAugmentedRealityPoint() { mPoi = new Pikachu( getString(R.string. p_name ), TARGET_LATITUDE, TARGET_LONGITUDE ); } /*вычисляем дистанцию между устройством и покемоном по формуле, о которой я говорил в начале урока. Результат приходит в десятичных градусах, умножение его на 100000 дает некую условную единицу, приблизительно равную 0.9м. Чтобы перевести результат в метрическую систему, нужно применять сложные расчеты, и я решил не усложнять приложение.*/ public double calculateDistance() { double dX = mPoi .getPoiLatitude() - mMyLatitude; double dY = mPoi .getPoiLongitude() - mMyLongitude; double distance = (Math. sqrt(Math.pow (dX, 2 ) + Math.pow(dY, 2 )) * 100000 ); return distance; } /*вычисляем теоретический азимут по формуле, о которой я говорил в начале урока. Вычисление азимута для разных четвертей производим на основе таблицы. */ public double calculateTeoreticalAzimuth() { double dX = mPoi .getPoiLatitude() - mMyLatitude; double dY = mPoi .getPoiLongitude() - mMyLongitude ; double phiAngle; double tanPhi; double azimuth = 0; tanPhi = Math.abs (dY / dX); phiAngle = Math.atan (tanPhi); phiAngle = Math.toDegrees (phiAngle); if (dX > 0 && dY > 0) { // I quater return azimuth = phiAngle; } else if (dX < 0 && dY > 0) { // II return azimuth = 180 - phiAngle; } else if (dX < 0 && dY < 0) { // III return azimuth = 180 + phiAngle; } else if (dX > 0 && dY < 0) { // IV return azimuth = 360 - phiAngle; } return phiAngle; } //расчитываем точность азимута, необходимую для отображения покемона private List<Double> calculateAzimuthAccuracy( double azimuth) { double minAngle = azimuth - AZIMUTH_ACCURACY ; double maxAngle = azimuth + AZIMUTH_ACCURACY ; List<Double> minMax = new ArrayList<Double>(); if (minAngle < 0) minAngle += 360; if (maxAngle >= 360) maxAngle -= 360; minMax.clear(); minMax.add(minAngle); minMax.add(maxAngle); return minMax; } //Метод isBetween определяет, находится ли азимут в целевом диапазоне с учетом допустимых отклонений private boolean isBetween( double minAngle, double maxAngle, double azimuth) { if (minAngle > maxAngle) { if (isBetween( 0, maxAngle, azimuth) && isBetween(minAngle, 360 , azimuth)) return true ; } else { if (azimuth > minAngle && azimuth < maxAngle) return true ; } return false; } // выводим на экран основную информацию о местоположении цели и нашего устройства private void updateDescription() { long distance = ( long ) calculateDistance(); int tAzimut = ( int ) mAzimuthTeoretical ; int rAzimut = ( int ) mAzimuthReal ; String text = mPoi.getPoiName() + " location:" + "\n latitude: " + TARGET_LATITUDE + " longitude: " + TARGET_LONGITUDE + "\n Current location:" + "\n Latitude: " + mMyLatitude + " Longitude: " + mMyLongitude + "\n " + "\n Target azimuth: " + tAzimut + " \n Current azimuth: " + rAzimut + " \n Distance: " + distance; descriptionTextView.setText(text); } /*переопределяем метод слушателя OnAzimuthChangeListener, который вызывается при изменении азимута устройства, расчитанного на основании показаний датчиков, получаемых в параметрах этого метода из класса MyCurrentAsimuth. Получаем данные азимута устройства, сравниваем их с целевыми параметрами - проверяем, если азимуты реальный и теоретический, а также дистанция до цели совпадают в пределах допустимых значений, отображаем картинку покемона на экране. Также вызываем метод обновления информации о местоположении на экране.*/ @Override public void onAzimuthChanged( float azimuthChangedFrom, float azimuthChangedTo) { mAzimuthReal = azimuthChangedTo; mAzimuthTeoretical = calculateTeoreticalAzimuth(); int distance = ( int ) calculateDistance(); pointerIcon = (ImageView) findViewById(R.id. icon ); double minAngle = calculateAzimuthAccuracy(mAzimuthTeoretical ).get( 0); double maxAngle = calculateAzimuthAccuracy(mAzimuthTeoretical ).get( 1); if ((isBetween(minAngle, maxAngle, mAzimuthReal )) && distance <= DISTANCE_ACCURACY ) { pointerIcon.setVisibility(View. VISIBLE ); } else { pointerIcon.setVisibility(View. INVISIBLE ); } updateDescription(); } /*переопределяем метод onLocationChanged интерфейса слушателя OnLocationChangedListener, здесь при изменении местоположения отображаем тост с новыми координатами и вызываем метод, который выводит основную информацию на экран.*/ @Override public void onLocationChanged(Location location) { mMyLatitude = location.getLatitude(); mMyLongitude = location.getLongitude(); mAzimuthTeoretical = calculateTeoreticalAzimuth(); Toast.makeText (this , "latitude: "+location.getLatitude()+ " longitude: "+location.getLongitude(), Toast. LENGTH_SHORT ).show(); //если устройство возвращает азимут = 0 отображаем картинку на основе значения дистанции if (mAzimuthReal == 0){ if ( distance <= DISTANCE_ACCURACY) { pointerIcon.setVisibility(View.VISIBLE); } else { pointerIcon.setVisibility(View.INVISIBLE); } } updateDescription(); } /*в методе жизненного цикла onStop мы вызываем методы отмены регистрации датчика азимута и закрытия подключения к службам Google Play*/ @Override protected void onStop() { myCurrentAzimuth.stop(); myCurrentLocation.stop(); super.onStop(); } //в методе onResume соответственно открываем подключение и регистрируем слушатель датчиков @Override protected void onResume() { super.onResume(); myCurrentAzimuth.start(); myCurrentLocation.start(); } /*метод setupListeners служит для инициализации слушателей местоположения и азимута - здесь мы вызываем конструкторы классов MyCurrentLocation и MyCurrentAzimuth и выполняем их методы start*/ private void setupListeners() { myCurrentLocation = new MyCurrentLocation( this); myCurrentLocation.buildGoogleApiClient( this ); myCurrentLocation.start(); myCurrentAzimuth = new MyCurrentAzimuth( this, this); myCurrentAzimuth.start(); } //метод setupLayout инициализирует все элементы экрана и создает surfaceView для отображения превью камеры private void setupLayout() { descriptionTextView = (TextView) findViewById(R.id.cameraTextView ); btnMap = (Button) findViewById(R.id. btnMap ); btnMap.setVisibility(View. VISIBLE ); btnMap.setOnClickListener( this ); getWindow().setFormat(PixelFormat. UNKNOWN); SurfaceView surfaceView = (SurfaceView) findViewById(R.id.cameraview ); mSurfaceHolder = surfaceView.getHolder(); mSurfaceHolder.addCallback( this ); mSurfaceHolder.setType(SurfaceHolder. SURFACE_TYPE_PUSH_BUFFERS ); } /*вызывается сразу же после того, как были внесены любые структурные изменения (формат или размер) surfaceView. Здесь , в зависимости от условий, стартуем или останавливаем превью камеры*/ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if ( isCameraviewOn ) { mCamera.stopPreview(); isCameraviewOn = false ; } if ( mCamera != null ) { try { mCamera .setPreviewDisplay( mSurfaceHolder); mCamera .startPreview(); isCameraviewOn = true ; } catch (IOException e) { e.printStackTrace(); } } } /*вызывается при первом создании surfaceView, здесь получаем доступ к камере и устанавливаем ориентацию дисплея превью*/ @Override public void surfaceCreated(SurfaceHolder holder) { mCamera = Camera. open(); mCamera.setDisplayOrientation( 90); } //вызывается перед уничтожением surfaceView, останавливаем превью и освобождаем камеру @Override public void surfaceDestroyed(SurfaceHolder holder) { mCamera.stopPreview(); mCamera.release(); mCamera = null ; isCameraviewOn = false ; } //и последний метод - обработчик нажатия кнопки, здесь по нажатию открываем карту @Override public void onClick(View v) { Intent intent = new Intent( this , MapActivity. class); startActivity(intent); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="info.fandroid.example.augmentedreality" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="18" android:targetSdkVersion="22" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.AppCompat.Light"> <activity android:name="info.fandroid.example.augmentedreality.CameraViewActivity" android:label="@string/title_activity_camera_view"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="info.fandroid.example.augmentedreality.MapActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="info.fandroid.example.augmentedreality.CameraViewActivity" /> </activity> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="AIzaSyCGGfjXTiou9cLPOoQ3kFBpMpf3eMU1-5w" /> </application> </manifest> |
android.permission.ACCESS_FINE_LOCATION
– позволяет максимально точно определять местоположение, используя все доступные способы: GPS, Wi-Fi и сеть сотовой связи- ACCESS_COARSE_LOCATION — позволяет приложению получить доступ к приблизительному местоположению.
- com.google.android.providers.gsf.permission.READ_GSERVICES позволяет работать с гулокартами
Приветствую, ребят поделитесь рабочим исходником или может есть уже готовый на гитхабе?
Посмотрите источник (ссылка в конце) — он обновлен, там есть новые исходники.
Добрый день. Для андроид 6 и выше нужно заменить: «mCamera = Camera. open();» на «if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)
//ask for authorisation
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 50);
else
{
mCamera = Camera. open();}» т.к. для подключения к камере требуется интерактивное подтверждение пользователя.
Добрый вечер. Интересует вопрос определения текущего азимута. При повороте телефона горизонтально на 90 градусов. Текущий азимут изменяется на 90 градусов. И получается, если мы смотрим в одну точку но в разном положении, у нас будут разные азимуты. Как это можно исправить?