Build your measurement app in Android Studio

The AR technology opens up a wide array of possibilities for business and its services. AR apps take the industry to the next level for the brand experience. Augmented Reality plays a key role in driving customer engagements in business marketing that significantly improves business ROI.

Today we are going to develop the AR measurement app for Android can measure either height or length. You simply need to point the phone’s camera to an object, then pick two points to measure the distance in between. This app uses augmented reality technology (AR) to measure the distance between a real-world object with your smartphone’s camera.

Android Project Structure

Add the following dependency in your project’s build.gradle:

classpath 'com.google.ar.sceneform:plugin:1.10.0'

Add the following in your app’s build.gradle :

implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.10.0'

You need to set the ar plugin in your app’s Gradle file as well.
Add the following below the dependencies:

apply plugin: 'com.google.ar.sceneform.plugin'

Add the following lines at the end of the app’s build.gradle:

sceneform.asset('sampledata/cube.fbx',         
'default',
'sampledata/cube.sfa',
'src/main/assets/cube')

The build.gradle of the app looks like this finally:

apply plugin: 'com.android.application'
apply plugin: 'com.google.ar.sceneform.plugin'

android {
compileSdkVersion 29
buildToolsVersion "29.0.0"
defaultConfig {
applicationId "com.example.armeasure"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

implementation "com.google.ar.sceneform.ux:sceneform-ux:1.10.0"
}

apply plugin: 'com.google.ar.sceneform.plugin'

sceneform.asset('sampledata/cube.fbx',
'default',
'sampledata/cube.sfa',
'src/main/res/raw/cube')

Android ARCore Sceneform requires Java 8 or higher version.

The sja and sjb are the Sceneform Asset Description and Sceneform Binary files. The sjb file is visible in the 3D viewer. It is shipped with the APK. and sja file is used to set properties for the sjb file.

The cube 3d object will be used to mark the points in the real world and find the length between them.

Android AR Project Code

To configure ARCore in your application add the following permission and metadata to your AndroidManifest.xml file.

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera.ar"
android:required="true" />

metadata goes inside the application tag.

<meta-data
android:name="com.google.ar.core"
android:value="required" />

The code for the AndroidManifest.xml is given below:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.armeasure">

<uses-permission android:name="android.permission.CAMERA" />

<uses-feature
android:name="android.hardware.camera.ar"
android:required="true" />

<application
android:allowBackup="true"
android:icon="@drawable/vmeasure_icon"
android:label="@string/app_name"
android:roundIcon="@drawable/vmeasure_icon"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.google.ar.core"
android:value="required" />
<activity
android:name=".MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

The code for the activity_main.xml class is given below:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".MainActivity">

<fragment
android:id="@+id/ux_fragment"
android:name="com.google.ar.sceneform.ux.ArFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ImageButton
android:id="@+id/btn_share"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="right"
android:layout_margin="10dp"
android:background="@drawable/ic_button_small_icon"
android:src="@drawable/ic_share_black_40dp"
/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="75dp"
android:background="@color/translucent_black">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textSize="30sp"
android:textColor="#fff"
android:text="Select width or height"
android:layout_marginVertical="10dp"/>
</LinearLayout>



<SeekBar
android:id="@+id/sk_height_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="100dp"
android:min="0"
android:max="500"
android:enabled="false"
android:progress="10"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">

<Button
android:id="@+id/btn_width"
android:layout_width="150dp"
android:layout_height="75dp"
android:text="Width"
android:background="@drawable/ic_button_round_green"
android:textColor="@color/white"
android:layout_weight="1"
android:layout_margin="10dp"
android:textSize="20dp"/>

<Button
android:id="@+id/btn_save"
android:layout_width="150dp"
android:layout_height="75dp"
android:text="Save"
android:background="@drawable/ic_button_round_orange"
android:textColor="@color/white"
android:layout_weight="1"
android:layout_margin="10dp"
android:textSize="20dp"
/>

<Button
android:id="@+id/btn_height"
android:layout_width="150dp"
android:layout_height="75dp"
android:text="Height"
android:background="@drawable/ic_button_round_green"
android:textColor="@color/white"
android:layout_weight="1"
android:layout_margin="10dp"
android:textSize="20dp"
/>
</LinearLayout>

</FrameLayout>

We’ve set the fragment as ArFragment. The ArFragment checks if the phone is compatible with ARCore. It checks if the camera permission is granted. If it isn’t, it’ll ask for it automatically. Also ask you to download the ARCore by Google Application.

The code for MainActivity.java class is given below:

package com.example.armeasure;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import com.google.ar.core.Anchor;
import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.core.Pose;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.ux.ArFragment;
import com.google.ar.sceneform.ux.TransformableNode;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

private static final String TAG = MainActivity.class.getSimpleName();
private static final double MIN_OPENGL_VERSION = 3.0;
private float upDistance = 0f;
private ArFragment arFragment;
private ModelRenderable andyRenderable;
private AnchorNode myanchornode;
private DecimalFormat form_numbers = new DecimalFormat("#0.00 m");

private Anchor anchor1 = null, anchor2 = null;

private HitResult myhit;

private TextView text;
private SeekBar sk_height_control;
private Button btn_save, btn_width, btn_height;
private ImageButton btn_share;

List<AnchorNode> anchorNodes = new ArrayList<>();
private boolean measure_height = false;
private ArrayList<String> arl_saved = new ArrayList<String>();
private float fl_measurement = 0.0f;
private String message;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

if (!checkIsSupportedDeviceOrFinish(this)) {
return;
}
try
{
this.getSupportActionBar().hide();
}
catch (NullPointerException e){}

setContentView(R.layout.activity_main);


arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
text = (TextView) findViewById(R.id.text);

sk_height_control = (SeekBar) findViewById(R.id.sk_height_control);
btn_height = (Button) findViewById(R.id.btn_height);
btn_save = (Button) findViewById(R.id.btn_save);
btn_width = (Button) findViewById(R.id.btn_width);
btn_share = (ImageButton) findViewById(R.id.btn_share);

sk_height_control.setEnabled(false);

btn_width.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
resetLayout();
measure_height = false;
text.setText("Click the extremes you want to measure");
}
});

btn_height.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
resetLayout();
measure_height = true;
text.setText("Click the base of the object you want to measure");
}
});

btn_save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(fl_measurement != 0.0f)
saveDialog();
else
Toast.makeText(MainActivity.this, "Make a measurement before saving", Toast.LENGTH_SHORT).show();
}
});

btn_share.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(arl_saved.size() > 0){
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
String shareBody = "";
for(String measurement : arl_saved)
shareBody += measurement+"\n";
shareBody = shareBody.trim();
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "AR Measurements");
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
startActivity(Intent.createChooser(sharingIntent, "Share via"));
}
else
Toast.makeText(MainActivity.this, "Save measurements before sharing", Toast.LENGTH_SHORT).show();
}
});


sk_height_control.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
upDistance = progress;
fl_measurement = progress/100f;
text.setText("Height: "+form_numbers.format(fl_measurement));
myanchornode.setLocalScale(new Vector3(1f, progress/10f, 1f));
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});

ModelRenderable.builder()
.setSource(this, R.raw.cube)
.build()
.thenAccept(renderable -> andyRenderable = renderable)
.exceptionally(
throwable -> {
Toast toast =
Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
return null;
});

arFragment.setOnTapArPlaneListener(
(HitResult hitResult, Plane plane, MotionEvent motionEvent) -> {
if (andyRenderable == null) {
return;
}
myhit = hitResult;

// Create the Anchor.
Anchor anchor = hitResult.createAnchor();
AnchorNode anchorNode = new AnchorNode(anchor);
anchorNode.setParent(arFragment.getArSceneView().getScene());

if(!measure_height) {
if(anchor2 != null){
emptyAnchors();
}
if (anchor1 == null) {
anchor1 = anchor;
} else {
anchor2 = anchor;
fl_measurement = getMetersBetweenAnchors(anchor1, anchor2);
text.setText("Width: " +
form_numbers.format(fl_measurement));
}
}
else{
emptyAnchors();
anchor1 = anchor;
text.setText("Move the slider till the cube reaches the upper base");
sk_height_control.setEnabled(true);
}

myanchornode = anchorNode;
anchorNodes.add(anchorNode);

// Create the transformable andy and add it to the anchor.
TransformableNode andy = new TransformableNode(arFragment.getTransformationSystem());
andy.setParent(anchorNode);
andy.setRenderable(andyRenderable);
andy.select();
andy.getScaleController().setEnabled(false);
});
}

/**
* Function to raise an object perpendicular to the ArPlane a specific distance
*
@param an anchor belonging to the object that should be raised
*
@param up distance in centimeters the object should be raised vertically
*/
private void ascend(AnchorNode an, float up) {
Anchor anchor = myhit.getTrackable().createAnchor(
myhit.getHitPose().compose(Pose.makeTranslation(0, up / 100f, 0)));

an.setAnchor(anchor);
}

/**
* Function to return the distance in meters between two objects placed in ArPlane
*
@param anchor1 first object's anchor
*
@param anchor2 second object's anchor
*
@return the distance between the two anchors in meters
*/
private float getMetersBetweenAnchors(Anchor anchor1, Anchor anchor2) {
float[] distance_vector = anchor1.getPose().inverse()
.compose(anchor2.getPose()).getTranslation();
float totalDistanceSquared = 0;
for (int i = 0; i < 3; ++i)
totalDistanceSquared += distance_vector[i] * distance_vector[i];
return (float) Math.sqrt(totalDistanceSquared);
}


/**
* Check whether the device supports the tools required to use the measurement tools
*
@param activity
*
@return boolean determining whether the device is supported or not
*/
private boolean checkIsSupportedDeviceOrFinish(final Activity activity) {
if (Build.VERSION.SDK_INT < VERSION_CODES.N) {
Log.e(TAG, "Sceneform requires Android N or later");
Toast.makeText(activity, "Sceneform requires Android N or later", Toast.LENGTH_LONG).show();
activity.finish();
return false;
}
String openGlVersionString =
((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE))
.getDeviceConfigurationInfo()
.getGlEsVersion();
if (Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) {
Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later");
Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG)
.show();
activity.finish();
return false;
}
return true;
}

private void saveDialog() {
AlertDialog.Builder mBuilder = new AlertDialog.Builder(MainActivity.this);
View mView = getLayoutInflater().inflate(R.layout.dialog_save, null);

EditText et_measure = (EditText) mView.findViewById(R.id.et_measure);
mBuilder.setTitle("Measurement title");

mBuilder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if(et_measure.length() != 0){
arl_saved.add(et_measure.getText()+": "+form_numbers.format(fl_measurement));
dialogInterface.dismiss();
}
else
Toast.makeText(MainActivity.this, "Title can't be empty", Toast.LENGTH_SHORT).show();
}
});

mBuilder.setView(mView);
AlertDialog dialog = mBuilder.create();

dialog.show();
}

/**
* Set layout to its initial state
*/
private void resetLayout(){
sk_height_control.setProgress(10);
sk_height_control.setEnabled(false);
measure_height = false;
emptyAnchors();
}

private void emptyAnchors(){
anchor1 = null;
anchor2 = null;
for (AnchorNode n : anchorNodes) {
arFragment.getArSceneView().getScene().removeChild(n);
n.getAnchor().detach();
n.setParent(null);
n = null;
}
}
}

The anchor is the position where the model or the Node is placed in the screen. It’s the center of the screen here.

In the addObject() the ARFragment gets the hit points on the plane using motion tracking.

placeObject() asynchronously sets the node to the center of the screen.

ModelRenderable.builder()is the method used to render the 3d model.

getMetersBetweenAnchors()is the method used to find the distance between the points.

There you have it! Your own measurement app in Android studio :)

I hope you had fun reading and/or following along. In the next story, we will look into how to build more features and interactions into this App. Stay tuned!

If you are interested in further exploring, here are some resources I found helpful along the way:

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store