Делаем приложение с дополненной реальностью как PokemonGo

В очередном выпуске “Как создать андроид-приложение” мы рассмотрим приложение с эффектом дополненной реальности, как игра PokemonGo. Да, да, мы тоже будем с вами ловить покемонов, привязанных к определенным координатам на местности, используя датчики android устройства.

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

  1. совмещает виртуальное и реальное;
  2. взаимодействует в реальном времени;
  3. работает в 3D.

Не путайте дополненную реальность с виртуальной реальностью – это разные технологии. Отличие заключается в том, что дополненная реальность – это наложение оцифрованной информации на реальный мир. Давайте рассмотрим простое приложение, которое отображает привязанную к координатам картинку на CameraView. Картинка отображается поверх изображения с камеры, если устройство находится поблизости и повернуто в сторону координат расположения картинки. С координатами местоположения понятно, но каким образом устройство будет определять направление расположения устройства относительно точки привязки картинки? В этом нам поможет теория геодезии и такое понятие, как азимут.

Немного теории

Кто не знает, азимут – это угол, образуемый заданным направлением движения и направлением на север.

Azimut_ru.svg
Источник: https://ru.wikipedia.org/wiki/Азимут

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

  • получить местоположение устройства
  • получить местоположение точки назначения
  • вычислить расстояние до точки назначения
  • рассчитать теоретический азимут
  • получить реальный азимут устройства
  • сравнить оба азимута
  • вызвать событие при совпадении значений азимута и расстояния в пределах допустимой погрешности

Теперь вопрос заключается в том, как вычислить азимут и расстояние. Это довольно просто, потому что мы будем игнорировать кривизну Земли и рассматривать ее как плоскую поверхность:

Источник: https://www.netguru.co/blog/augmented-reality-mobile-android
Источник: https://www.netguru.co/blog/augmented-reality-mobile-android

Как вы можете видеть, у нас есть прямоугольный треугольник, и мы можем вычислить угол ᵠ между точками  с помощью простого уравнения:

2016-08-17_22-23-34

В приведенной таблице представлены отношения между уголом в градах и азимутом A(AB):

 

2016-08-18_11-53-31
Источник: https://www.netguru.co/blog/augmented-reality-mobile-android

А чтобы определить расстояние до точки назначения, воспользуемся этой формулой:

2016-08-17_22-29-33

Рассмотрим реализацию нашего приложения

Ниже код проекта в Андроид Студио, который мы сейчас подробно разберем.

Для начала посмотрим ресурсы. Нам понадобится изображение покемона.

Вот ссылка на ресурсы: https://yadi.sk/d/Y06QyQ6juJibC

В нашем приложении 2 активити, поэтому нам нужны 2 макета компоновки для них. В одном будет отображаться превью камеры с наложением текста и картинки, а также кнопка для перехода на карту.

<?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.

<?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 в приложение,  урок об этом смотрите на нашем сайте: ссылка

Теперь перейдем к коду. Нам понадобятся два интерфейса. Первый –  это слушатель изменения местоположения.

import android.location.Location;

public interface OnLocationChangedListener {
    void onLocationChanged(Location currentLocation);
}

Второй – слушатель изменения азимута.

public interface OnAzimuthChangedListener {
    void onAzimuthChanged( float azimuthFrom, float azimuthTo);
}

В классе MyCurrentLocation будем определять местоположение устройства. Этот класс реализует такие интерфейсы: GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener.

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.

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. Его задача – отображение карты с меткой в месте привязки покемона.

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 – здесь просто набор переменных: имя и координаты, а также геттеры для них.

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.

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);
	}
}
В методе onCreate вызываем методы setupListeners(); setupLayout(); setAugmentedRealityPoint();
Теперь посмотрим манифест приложения:
<?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 позволяет работать с гулокартами
Далее описаны 2 активити, причем для MapActivity указан атрибут android :name=”android.support.PARENT_ACTIVITY”, определяющий его как дочернее по отношению к главному активити. Этот подход упрощает навигацию, размещая в экшнбаре MapActivity стрелку, ведущую назад к главному активити.
Также здесь видим секцию meta-data, где прописаны версия google_play_services и API_KEY для работы Google карт.
Как я уже говорил, для работы с Google Maps нужно получить apikey в консоли разработчика Google и прописать его здесь, в манифесте приложения. Ссылки на инструкции выше по тексту.
Итак наше приложение готово к запуску. Тестировать его нужно на реальном устройстве. Запускаем приложение и видим на экране превью камеры, а на нем – обновляющуюся информацию о местоположении и растоянии до цели, а также азимуты. Азимуты отображаются в градусах, а дистанция в условных единицах, примерно равных 0.9 м. В своем приложении я указал для покемона местоположение, которое совпадает с моим, поэтому на экране дистанция равна 0, что соответствует диапазону, указанному в условиии. А при совпадении азимутов в пределах константы в поле зрения камеры на экране появляется изображение покемона.
Чтобы посмотреть местоположение покемона на карте, нажмите кнопку Map. Открывается карта с меткой, указывающей местоположение покемона. А при нажатии метки появляется кнопка приложения GoogleMaps (если оно установлено на устройстве) с возможнстью открыть метку в приложении Google карты или расчитать маршрут до цели. Очень удобно, если вы разместили покемона далеко от вас.
device-2016-08-18-135749
Это я тестирую приложение на устройстве Nexus-5 которое имеет большой набор датчиков и без проблем расчитывает азимут. А если запустить приложение на Lenovo-820, например, то выяснится, что реальный азимут равен 0. Он не расчитывается, так на устройстве как нет необходимого сенсора. Расчитывается только дистанция до цели. Поэтому сейчас изображение покемона показывается на экране постоянно, независимо от того, куда я поворачиваю устройство. Но если я отойду от этой точки на дистанцию более 5 единиц, покемон исчезнет с экрана.
Ну вот , если вы до сих пор не бегали за покемонами по округе – у вас появился повод это сделать. вы можете тестировать приложение, меняя параметры, такие как целевое местоположение и константы допустимых значений. Экспериментируйте при получении и расчете данных сенсоров. добавляйте новые условия, расширяйте функционал приложения. например, в качестве простого домашнего задания добавьте возможность отображения на карте вашего местоположения. Ссылку на урок об этом найдете выше по тексту, где инструкция на работу с картами.
Коментарі: 4
  1. Алексей
    Алексей

    Приветствую, ребят поделитесь рабочим исходником или может есть уже готовый на гитхабе?

    1. Виталий Непочатов
      admin (автор)

      Посмотрите источник (ссылка в конце) – он обновлен, там есть новые исходники.

  2. Gleb_NSK
    Gleb_NSK

    Добрый день. Для андроид 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();}” т.к. для подключения к камере требуется интерактивное подтверждение пользователя.

  3. Иван

    Добрый вечер. Интересует вопрос определения текущего азимута. При повороте телефона горизонтально на 90 градусов. Текущий азимут изменяется на 90 градусов. И получается, если мы смотрим в одну точку но в разном положении, у нас будут разные азимуты. Как это можно исправить?

Додати коментар