This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Windows Phone 7 that provide information about the outside world.
With the user's permission, the location service lets your application
obtain the phone's location on the earth in the traditional geographic
coordinates of longitude and latitude, whereas the accelerometer tells
your program which way is down. The accelerometer and location service are related in that neither of them
will work very well in outer space.
Accelerometer
Windows Phones contain an accelerometer-a small hardware device that
essentially measures force, which elementary physics tells us is proportional to
acceleration. It is convenient to represent the accelerometer output as a vector
in three-dimensional space. Vectors are commonly written in boldface, so the
acceleration vector can be symbolized as
(x,
y,
z).
XNA defines a three-dimensional vector type; Silverlight does not.
The magnitude of the vector
(x,
y,
z)
is calculable from the three-dimensional form of the Pythagorean
Theorem:
This is a traditional three-dimensional coordinate system, the same
coordinate system used in XNA 3D programming. It's termed a
right-hand coordinate
system: Point the index finger of your right hand to increasing X, the middle
finger to increase Y, and your thumb points to increasing Z. Or, curve the
fingers of your right hand from the positive X axis to the positive Y axis. Your
thumb again points to increasing Z.
When the phone is still, the accelerometer vector points towards the Earth.
The magnitude is 1, meaning 1 g,
which is the force of gravity on the earth's surface. When holding your phone in
the upright position, the acceleration vector is
(0, -1, 0), that is,
straight down.
Turn the phone 90 dig. counter-clockwise (called landscape left) and the
acceleration vector becomes (-1,
0, 0), upside down it's
(0, 1, 0), and another 90 dig. counter-clockwise turn brings you to
the landscape right orientation and an accelerometer value of
(1, 0, 0). Sit the
phone down on the desk with the display facing up, and the acceleration vector
is (0, 0, -1).
In your program you create an instance of the
Accelerometer class,
set an event handler for the ReadingChanging
event, and call
Start.
And then it gets a little tricky. Let's take a look at a project named
SilverlightAccelerometer. that simply displays the current reading in its
content grid. A centered TextBlock
is defined in the XAML file:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<TextBlock
Name="txtblk"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
This is a program that will display the accelerometer vector throughout its
lifetime, so it creates the Accelerometer
class in its constructor and calls
Start:
public
MainPage()
{
InitializeComponent();
Accelerometer acc =
new Accelerometer();
acc.ReadingChanged += OnAccelerometerReadingChanged;
try
{
acc.Start();
}
catch (Exception
exc)
{
txtblk.Text = exc.Message;
}
}
The documentation warns that calling
Start might raise an
exception, so the program protects itself against that eventuality. The
Accelerometer also
supports Stop and
Dispose methods, but
this program doesn't make use of them. A
State property is also
available if you need to know if the accelerometer is available and what it's
currently doing.
A ReadingChanged
event is accompanied by the
AccelerometerReadingEventArgs
event arguments. A little background:
All the user-interface elements and objects in a Silverlight application are
created and accessed in a main thread of execution often called the
user interface thread
or the UI thread. An
instance of this Dispatcher
is readily available. The
DependencyObject class defines a property named
Dispatcher of type
Dispatcher, and many
Silverlight classes derive from DependencyObject. The
Dispatcher class defines a method named
CheckAccess that
returns true if you can
access a particular user interface object from the current thread.
The SilverlightAccelerometer project implements a syntactically elaborate
version of the code, but then I'll show you how to chop it down in size. The
verbose version requires a delegate and a method defined in accordance with that
delegate. The delegate (and method) should have no return value, but as many
arguments as you need to do the job, in this case the job of setting a string to
the Text property of a
TextBlock:
delegate
void
SetTextBlockTextDelegate(TextBlock
txtblk, string text);
void SetTextBlockText(TextBlock
txtblk, string text)
{
txtblk.Text =
text;
}
The OnAccelerometerReadingChanged
is responsible for calling
SetTextBlockText. It
first makes use of CheckAccess
to see if it can just call the
SetTextBlockText method directly. If not, then thehandler calls
the BeginInvoke
method.
The first argument is an instantiation of the delegate with the
SetTextBlockText
method; this is followed by all the arguments that
SetTextBlockText
requires:
void
OnAccelerometerReadingChanged(object sender,
AccelerometerReadingEventArgs args)
{
string str = String.Format("X
= {0:F2}\n" +
"Y =
{1:F2}\n" +
"Z =
{2:F2}\n\n" +
"Magnitude =
{3:F2}\n\n" +
"{4}",
args.X, args.Y, args.Z,
Math.Sqrt(args.X * args.X +
args.Y * args.Y + args.Z * args.Z), args.Timestamp);
if
(txtblk.CheckAccess())
{
SetTextBlockText(txtblk, str);
}
else
{
txtblk.Dispatcher.BeginInvoke(new
SetTextBlockTextDelegate(SetTextBlockText),
txtblk, str);
}
}
The Windows Phone 7 emulator doesn't contain any actual accelerometer, so it
always reports a value of (0, 0, -1), which indicates the phone is lying on a
flat surface. The program only makes sense when running on an actual phone:
The values here indicate the phone is roughly upright but tilted back a bit,
which is a very natural orientation in actual use.
Simple Bubble Level
One handy tool found in any workshop is a bubble level, also called a spirit
level. A little bubble always floats to the top of a liquid, so it visually
indicates whether something is parallel or orthogonal to the earth, or tilted in
some way. The XnaAccelerometer project includes a 48-by-48 pixel bitmap named
Bubble.bmp that consists of a red circle:
As with the Silverlight program, you'll need a reference
to the Microsoft.Devices.Sensors library and a using directive for the
Microsoft.Devices.Sensors
namespace. The fields in the
Game1 class mostly
involve variables necessary to position that bitmap on the screen:
namespace
XnaAccelerometer
{
public class
Game1 : Microsoft.Xna.Framework.Game
{
const float
BUBBLE_RADIUS_MAX = 25;
const float
BUBBLE_RADIUS_MIN = 12;
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Vector2 screenCenter;
float screenRadius;
// less BUBBLE_RADIUS_MAX
Texture2D bubbleTexture;
Vector2 bubbleCenter;
Vector2 bubblePosition;
float bubbleScale;
Vector3 accelerometerVector;
object accelerometerVectorLock =
new object();
public Game1()
{
graphics = new
GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for
Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected
override void Initialize()
{
Accelerometer accelerometer =
new Accelerometer();
accelerometer.ReadingChanged += OnAccelerometerReadingChanged;
try
{
accelerometer.Start();
}
catch
{
}
base.Initialize();
}
void OnAccelerometerReadingChanged(object
sender, AccelerometerReadingEventArgs args)
{
lock (accelerometerVectorLock)
{
accelerometerVector = new
Vector3((float)args.X,
(float)args.Y, (float)args.Z);
}
}
protected
override void LoadContent()
{
spriteBatch = new
SpriteBatch(GraphicsDevice);
Viewport viewport =
this.GraphicsDevice.Viewport;
screenCenter = new
Vector2(viewport.Width / 2, viewport.Height /
2);
screenRadius = Math.Min(screenCenter.X,
screenCenter.Y) - BUBBLE_RADIUS_MAX;
bubbleTexture = this.Content.Load<Texture2D>("Bubble");
bubbleCenter = new
Vector2(bubbleTexture.Width / 2,
bubbleTexture.Height / 2);
}
protected
override void Update(GameTime
gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
Vector3 accVector;
lock (accelerometerVectorLock)
{
accVector = accelerometerVector;
}
int sign =
this.Window.CurrentOrientation ==
DisplayOrientation.LandscapeLeft ? 1 : -1;
bubblePosition = new
Vector2(screenCenter.X + sign * screenRadius
* accVector.Y,
screenCenter.Y + sign * screenRadius *
accVector.X);
float bubbleRadius =
BUBBLE_RADIUS_MIN + (1 - accVector.Z) / 2 *
(BUBBLE_RADIUS_MAX - BUBBLE_RADIUS_MIN);
bubbleScale = bubbleRadius / (bubbleTexture.Width / 2);
base.Update(gameTime);
}
protected
override void Draw(GameTime
gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.Draw(bubbleTexture, bubblePosition,
null, Color.White,
0,
bubbleCenter, bubbleScale,
SpriteEffects.None, 0);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
OnAccelerometerReadingChanged
and Update
run in separate threads. One is setting the field;
the other is accessing the field. This is no problem if the field is set or
accessed in a single machine code instruction.
While that could hardly be classified as a catastrophe
in this program, let's play it entirely safe and use the C#
lock statement to make
sure the Vector3
value is stored and retrieved by the two threads without
interruption. That's the purpose of the
accelerometerVectorLock variable among the fields.
The LoadContent
method loads the bitmap used for the bubble and
initializes several variables used for positioning the bitmap:
The Update
method safely access the
accelerometerVector
field and calculates bubblePosition
based on the X
and Y
components. It might seem like I've mixed up the
X and
Y components in the
calculation, but that's because the default screen orientation is portrait in
XNA, so it's opposite the coordinates of the acceleration vector. Because both
landscape modes are supported by default, it's also necessary to multiply the
acceleration vector values by -1 when the phone has been tilted into the
LandscapeRight mode:
The program doesn't look like much, and is even more
boring running on the emulator. Here's an indication that the phone is roughly
upright and tilted back a bit: