3.1 图片资源
1.高分辨率图片
ball.png------震动传感器使用的球体图片。
icon------本应用程序的图标(在高分辨率屏幕上使用)。
wood.jpg------应用程序主窗口背景。
2. 中分辨率图片
icon.png------本应用程序的图标(在中分辨率屏幕上使用)。
3. 低分辨率图片
icon.png------应用程序在低分辨率屏幕上使用的图标。
3.2 布局资源(main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
</LinearLayout>
<LinearLayout>:用单列或单行的方式来排列布局中的子元素。
xmlns:android:固定设置,始终是http://schemas.android.com/apk/res/android
android:orientation=”vertical”:指示用一列的方式排列布局中的子元素。
android:layout_width=”fill_parent”:定义当前View在屏幕上可以占据的宽度,fill_parent即横行完全填充
android:layout_height=”fill_parent”:定义当前View在屏幕上可以占据的高度,fill_parent即纵向完全填充
3.3 字符资源(strings.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AccelerometerPlay</string>
</resources>
定义应用程序在应用启动器中显示的名称。
四.代码分析
package com.example.android.accelerometerplay;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.BitmapFactory.Options;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
public class AccelerometerPlayActivity extends Activity {
//定制的View对象,继承View类,并实现SensorEventListener接口。
private SimulationView mSimulationView;
//Android系统传感器管理器,通过调用getSystemService()方法来获取管理器的一个实例
private SensorManager mSensorManager;
//Android系统的电源管理器,本例中使用电源管理器获取系统的唤醒锁
private PowerManager mPowerManager;
//Android系统的窗口管理器,提供了与应用会话的接口,本例使用窗口管理器获取当前默认的显示对象
private WindowManager mWindowManager;
//Android系统用于提供显示对象的尺寸、分辨、角度等信息,本例用于获取设备的旋转角度
private Display mDisplay;
//Android系统的唤醒锁,本例使用唤醒锁,锁定屏幕的显示,使程序在运行期间,屏幕始终高亮显示。
private WakeLock mWakeLock;
/** Activity被首次创建时,系统会调用这个方法,而且只在创建时调用一次,因此在本方法中完成一些
* 短时的、全局的初始化操作 */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取传感器管理器的一个实例
mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
// 获取电源管理器的一个实例
mPowerManager = (PowerManager) getSystemService(POWER_SERVICE);
// 获取窗口管理器的一个实例
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// 获取系统当前默认的显示对象
mDisplay = mWindowManager.getDefaultDisplay();
// 创建一个亮度唤醒锁
mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, getClass()
.getName());
// 实例化我们定制的模拟View对象,并把它设置为Activity的显示内容。
mSimulationView = new SimulationView(this);
setContentView(mSimulationView);
}
@Override
protected void onResume() {
super.onResume();
/*
* 当Activity恢复显示时,我们申请了一个唤醒锁,让屏幕在程序运行期间保持高亮,
* 因为用户不会喜欢在程序运行期间频繁的摆弄屏幕或按钮。
*/
mWakeLock.acquire();
// 启动模拟窗口
mSimulationView.startSimulation();
}
@Override
protected void onPause() {
super.onPause();
/*
* 当Activity被挂起时,我们要确保终止模拟窗口,并且要释放传感器资源和唤醒锁。
*/
// 终止模拟窗口
mSimulationView.stopSimulation();
// 释放唤醒锁
mWakeLock.release();
}
/*
* 内部类,震动器模拟窗口,继承View对象,并实现SensorEventListener接口的以下方法:
* onSensorChanged()
* onAccuracyChanged()
*/
class SimulationView extends View implements SensorEventListener {
// 以米为单位定义球的直径
private static final float sBallDiameter = 0.004f;
private static final float sBallDiameter2 = sBallDiameter * sBallDiameter;
//虚拟的桌面和空气的摩擦力
private static final float sFriction = 0.1f;
//传感器对象
private Sensor mAccelerometer;
//
private long mLastT;
private float mLastDeltaT;
private float mXDpi;
private float mYDpi;
private float mMetersToPixelsX;
private float mMetersToPixelsY;
private Bitmap mBitmap;
private Bitmap mWood;
private float mXOrigin;
private float mYOrigin;
//传感器的X轴位置
private float mSensorX;
//传感器的Y轴位置
private float mSensorY;
private long mSensorTimeStamp;
private long mCpuTimeStamp;
private float mHorizontalBound;
private float mVerticalBound;
private final ParticleSystem mParticleSystem = new ParticleSystem();
/*
* View对象的内部类,定义在View对象内运动的震动粒子。
* 每个粒子都有它的加速度的前一个位置和当前的位置。对于每个
* 实际添加的粒子,都有它们自己的摩擦系数。
*/
class Particle {
private float mPosX;
private float mPosY;
private float mAccelX;
private float mAccelY;
private float mLastPosX;
private float mLastPosY;
//摩擦系数
private float mOneMinusFriction;
/*
* 粒子对象的构造器
* 通过随机数使每个粒子的摩擦系统都有一点差异
*/
Particle() {
final float r = ((float) Math.random() - 0.5f) * 0.2f;
mOneMinusFriction = 1.0f - sFriction + r;
}
/*
* 计算粒子的物理特性,把物理特性强制应用到我们的虚拟对象向粒子上
* float sx:传感器X轴方向加速度
* float sy:传感器Y轴方向加速度
* float dT: 传感器每次响应时间差
* float dTC:本次时间差与上次时间差的比值
*/
public void computePhysics(float sx, float sy, float dT, float dTC) {
// 虚拟对象的质量
final float m = 1000.0f;
// 虚拟对象X轴方向的重力
final float gx = -sx * m;
// 虚拟对象Y轴方向的重力
final float gy = -sy * m;
/*
* 把公式 F=mA 转换成 A=F/m,我们能够通过完全消除所有公式中的“m”(质量)来简化代码
* 但是它也从这段示例代码中隐藏了概念
* 合力=质量乘以加速度
*/
final float invm = 1.0f / m;
// 虚拟对象X轴方向的加速度
final float ax = gx * invm;
// 虚拟对象Y轴方向的加速度
final float ay = gy * invm;
/*
* 此处公式不做解释
*/
final float dTdT = dT * dT;
// 虚拟对象在X轴方向位移后的位置
final float x = mPosX + mOneMinusFriction * dTC * (mPosX - mLastPosX) + mAccelX
* dTdT;
// 虚拟对象在Y轴方向位移后的位置
final float y = mPosY + mOneMinusFriction * dTC * (mPosY - mLastPosY) + mAccelY
* dTdT;
// 保存前次X轴位移后的位置
mLastPosX = mPosX;
// 保存前次Y轴位移后的位置
mLastPosY = mPosY;
// 保存当前X轴位移后的位置
mPosX = x;
// 保存当前Y轴位移后的位置
mPosY = y;
// 保存X轴加速度
mAccelX = ax;
// 保存Y轴加速度
mAccelY = ay;
}
/*
* 用Verlet积分器能很简单的解决限制和碰撞的问题,
* 我们只需要用这种方法来移动正在碰撞的或受到限制的粒子
*/
public void resolveCollisionWithBounds() {
// X轴最大边界
final float xmax = mHorizontalBound;
// Y轴最大边界
final float ymax = mVerticalBound;
// X轴当前位置
final float x = mPosX;
// Y轴当前位置
final float y = mPosY;
if (x > xmax) {
mPosX = xmax;
} else if (x < -xmax) {
mPosX = -xmax;
}
if (y > ymax) {
mPosY = ymax;
} else if (y < -ymax) {
mPosY = -ymax;
}
}
}
/*
* 内部类,一个View对象内粒子的集合
*/
class ParticleSystem {
// 定义当前集合内的粒子数
static final int NUM_PARTICLES = 15;
private Particle mBalls[] = new Particle[NUM_PARTICLES];
ParticleSystem() {
/*
* 初始化没有速度和加速度的粒子
*/
for (int i = 0; i < mBalls.length; i++) {
mBalls[i] = new Particle();
}
}
/*
* 使用Verlet积分器更新系统中每个粒子的位置
*/
private void updatePositions(float sx, float sy, long timestamp) {
final long t = timestamp;
if (mLastT != 0) {
final float dT = (float) (t - mLastT) * (1.0f / 1000000000.0f);
if (mLastDeltaT != 0) {
final float dTC = dT / mLastDeltaT;
final int count = mBalls.length;
for (int i = 0; i < count; i++) {
Particle ball = mBalls[i];
ball.computePhysics(sx, sy, dT, dTC);
}
}
mLastDeltaT = dT;
}
mLastT = t;
}
/*
* 执行一次传感器模拟的迭代。
* 首先更新所有的粒子的位置,并解决限制和碰撞的问题
* float sx: X轴加速度
* float sy: Y轴加速度
* long now: 传感器当前响应的时间
*/
public void update(float sx, float sy, long now) {
// 更新粒子系统中粒子的位置
updatePositions(sx, sy, now);
// 解决碰撞的最大迭代数
final int NUM_MAX_ITERATIONS = 10;
/*
* 以下解决粒子间的碰撞问题,每个粒子都要被检测是否碰撞了其他粒子
* 如果检测到了一个碰撞,那么使用弹簧运动的方式来移走这个粒子。
*/
boolean more = true;
final int count = mBalls.length;
for (int k = 0; k < NUM_MAX_ITERATIONS && more; k++) {
more = false;
for (int i = 0; i < count; i++) {
Particle curr = mBalls[i];
for (int j = i + 1; j < count; j++) {
Particle ball = mBalls[j];
float dx = ball.mPosX - curr.mPosX;
float dy = ball.mPosY - curr.mPosY;
float dd = dx * dx + dy * dy;
// Check for collisions
if (dd <= sBallDiameter2) {
/*
* 添加一个熵值
*/
dx += ((float) Math.random() - 0.5f) * 0.0001f;
dy += ((float) Math.random() - 0.5f) * 0.0001f;
dd = dx * dx + dy * dy;
// 模拟弹簧运动
final float d = (float) Math.sqrt(dd);
final float c = (0.5f * (sBallDiameter - d)) / d;
curr.mPosX -= dx * c;
curr.mPosY -= dy * c;
ball.mPosX += dx * c;
ball.mPosY += dy * c;
more = true;
}
}
/*
* 最后确保粒子跟边框没有交点
*/
curr.resolveCollisionWithBounds();
}
}
}
public int getParticleCount() {
return mBalls.length;
}
public float getPosX(int i) {
return mBalls[i].mPosX;
}
public float getPosY(int i) {
return mBalls[i].mPosY;
}
}
public void startSimulation() {
/*
* 通过使用低频率参数(SENSOR_DELAY_UI),我们不必高频的响应加速度传感器事件
* 我们获得一个低通过滤器,从这个过滤器中提取加速器的重力组件。
* 还会获得一个额外的好处:减少电源和CPU的使用率
*/
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI);
}
/*
* 终止模拟器工作
*/
public void stopSimulation() {
mSensorManager.unregisterListener(this);
}
public SimulationView(Context context) {
super(context);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mXDpi = metrics.xdpi;
mYDpi = metrics.ydpi;
mMetersToPixelsX = mXDpi / 0.0254f;
mMetersToPixelsY = mYDpi / 0.0254f;
// 调整粒子球的大小
Bitmap ball = BitmapFactory.decodeResource(getResources(), R.drawable.ball);
final int dstWidth = (int) (sBallDiameter * mMetersToPixelsX + 0.5f);
final int dstHeight = (int) (sBallDiameter * mMetersToPixelsY + 0.5f);
mBitmap = Bitmap.createScaledBitmap(ball, dstWidth, dstHeight, true);
Options opts = new Options();
opts.inDither = true;
opts.inPreferredConfig = Bitmap.Config.RGB_565;
mWood = BitmapFactory.decodeResource(getResources(), R.drawable.wood, opts);
}
/*
* 重写View类的onSizeChanged方法
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 计算相对图片原点的屏幕原点
mXOrigin = (w - mBitmap.getWidth()) * 0.5f;
mYOrigin = (h - mBitmap.getHeight()) * 0.5f;
mHorizontalBound = ((w / mMetersToPixelsX - sBallDiameter) * 0.5f);
mVerticalBound = ((h / mMetersToPixelsY - sBallDiameter) * 0.5f);
}
/*
* 实现SensorEventListener接口的onSensorChanged方法
* @see android.hardware.SensorEventListener#onSensorChanged(android.hardware.SensorEvent)
*/
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER)
return;
/*
* 记录加速传感器的数据、事件发生的时间以及当前时间。
* 我们需要后者来计算渲染期间的展现时间。在这个应用程序中,
* 我们需要考虑屏幕是怎样跟传感器一起旋转的(让它返回的数据
* 跟屏幕的原始坐标空间相对应)
*/
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0:
mSensorX = event.values[0];
mSensorY = event.values[1];
break;
case Surface.ROTATION_90:
mSensorX = -event.values[1];
mSensorY = event.values[0];
break;
case Surface.ROTATION_180:
mSensorX = -event.values[0];
mSensorY = -event.values[1];
break;
case Surface.ROTATION_270:
mSensorX = event.values[1];
mSensorY = -event.values[0];
break;
}
mSensorTimeStamp = event.timestamp;
mCpuTimeStamp = System.nanoTime();
}
/*
* 覆写View类的onDraw方法
* @see android.view.View#onDraw(android.graphics.Canvas)
*/
@Override
protected void onDraw(Canvas canvas) {
// 描画背景
canvas.drawBitmap(mWood, 0, 0, null);
// 基于加速度传感器的数据和展现时间计算粒子对象的新的位置
final ParticleSystem particleSystem = mParticleSystem;
final long now = mSensorTimeStamp + (System.nanoTime() - mCpuTimeStamp);
final float sx = mSensorX;
final float sy = mSensorY;
particleSystem.update(sx, sy, now);
final float xc = mXOrigin;
final float yc = mYOrigin;
final float xs = mMetersToPixelsX;
final float ys = mMetersToPixelsY;
final Bitmap bitmap = mBitmap;
final int count = particleSystem.getParticleCount();
for (int i = 0; i < count; i++) {
/*
* We transform the canvas so that the coordinate system matches
* the sensors coordinate system with the origin in the center
* of the screen and the unit is the meter.
*/
/*
* 变换坐标,让画布的坐标系统与传感器的坐标系统匹配,
* 以屏幕中央为原点,并且单位是“米”
*/
final float x = xc + particleSystem.getPosX(i) * xs;
final float y = yc - particleSystem.getPosY(i) * ys;
canvas.drawBitmap(bitmap, x, y, null);
}
// 确保画布重绘
invalidate();
}
/*
* 实现SensorEventListener接口的onAccuracyChanged方法
* @see android.hardware.SensorEventListener#onAccuracyChanged(android.hardware.Sensor, int)
*/
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
}