Android游戏开发之旅

来自个人维基
跳转至: 导航搜索

目录

Android游戏开发之旅一 长按Button原理

对于很多游戏使用屏幕控制一般需要考虑长按事件,比如在动作类的游戏中需要长按发射武器,结合Android Button模型,我们实现一个带图片的Button的长按,为了更清晰的显示原理,Android开发网这里使用ImageButton作为基类

  public class RepeatingImageButton extends ImageButton {
 
    private long mStartTime; //记录长按开始
 
    private int mRepeatCount; //重复次数计数
 
    private RepeatListener mListener;
 
    private long mInterval = 500; //Timer触发间隔,即每0.5秒算一次按下
 
 
 
    public RepeatingImageButton(Context context) {
 
        this(context, null);
 
    }
 
    public RepeatingImageButton(Context context, AttributeSet attrs) {
 
        this(context, attrs, android.R.attr.imageButtonStyle);
 
    }
 
    public RepeatingImageButton(Context context, AttributeSet attrs, int defStyle) {
 
        super(context, attrs, defStyle);
 
        setFocusable(true); //允许获得焦点
 
        setLongClickable(true); //启用长按事件
 
    }
 
 
 
    public void setRepeatListener(RepeatListener l, long interval) { //实现重复按下事件listener
 
        mListener = l;
 
        mInterval = interval; 
 
    }
 
 
 
    @Override
 
    public boolean performLongClick() {
 
        mStartTime = SystemClock.elapsedRealtime();
 
        mRepeatCount = 0;
 
        post(mRepeater);
 
        return true;
 
    }
 
 
 
 
 
    @Override
 
    public boolean onTouchEvent(MotionEvent event) {
 
        if (event.getAction() == MotionEvent.ACTION_UP) {  //   本方法原理同onKeyUp的一样,这里处理屏幕事件,下面的onKeyUp处理Android手机上的物理按键事件
 
              removeCallbacks(mRepeater);  
 
            if (mStartTime != 0) {
 
                doRepeat(true);
 
                mStartTime = 0;
 
            }
 
        }
 
        return super.onTouchEvent(event);
 
    }
 
  //处理导航键事件的中键或轨迹球按下事件
 
    @Override
 
    public boolean onKeyDown(int keyCode, KeyEvent event) {
 
        switch (keyCode) {
 
            case KeyEvent.KEYCODE_DPAD_CENTER: 
 
            case KeyEvent.KEYCODE_ENTER:
 
 
 
                super.onKeyDown(keyCode, event);
 
                return true;
 
        }
 
        return super.onKeyDown(keyCode, event);
 
    }
 
   //当按键弹起通知长按结束
 
    @Override
 
    public boolean onKeyUp(int keyCode, KeyEvent event) {
 
        switch (keyCode) {
 
        case KeyEvent.KEYCODE_DPAD_CENTER:
 
        case KeyEvent.KEYCODE_ENTER:
 
 
 
            removeCallbacks(mRepeater); //取消重复listener捕获
 
            if (mStartTime != 0) {
 
                doRepeat(true); //如果长按事件累计时间不为0则说明长按了
 
                mStartTime = 0; //重置长按计时器
 
            }
 
        }
 
        return super.onKeyUp(keyCode, event);
 
    }
 
 
 
    private Runnable mRepeater = new Runnable() {  //在线程中判断重复
 
        public void run() {
 
            doRepeat(false);
 
            if (isPressed()) {
 
                postDelayed(this, mInterval); //计算长按后延迟下一次累加
 
            }
 
        }
 
    };
 
    private  void doRepeat(boolean last) {
 
        long now = SystemClock.elapsedRealtime();
 
        if (mListener != null) {
 
            mListener.onRepeat(this, now - mStartTime, last ? -1 : mRepeatCount++);
 
        }
 
    }

下面是重复Button Listener接口的定义,调用时在Button中先使用setRepeatListener()方法实现RepeatListener接口

    public interface RepeatListener {
 
             void onRepeat(View v, long duration, int repeatcount); //参数一为用户传入的Button对象,参数二为延迟的毫秒数,第三位重复次数回调。
 
    }
 
}

本类大家可以直接在自己的View中implements实现RepeatListener接口即可。

Android游戏开发之旅二 View和SurfaceView

在Android游戏当中充当主要的除了控制类外就是显示类,在J2ME中我们用Display和Canvas来实现这些,而
Google Android中涉及到显示的为view类,Android游戏开发中比较重要和复杂的就是显示和游戏逻辑的处理。这里
我们说下android.view.View和android.view.SurfaceView。SurfaceView是从View基类中派生出来的显示类,直接子类有GLSurfaceView和VideoView,可以看出GL和视频播放以及Camera摄像头一般均使用SurfaceView,到底有哪些优势呢? SurfaceView可以控制表面的格式,比如大小,显示在屏幕中的位置,最关键是的提供了SurfaceHolder类,使用getHolder方法获取,相关的有Canvas lockCanvas()
Canvas lockCanvas(Rect dirty) 、void removeCallback(SurfaceHolder.Callback callback)、void unlockCanvasAndPost(Canvas canvas) 控制图形以及绘制,而在SurfaceHolder.Callback 接口回调中可以通过下面三个抽象类可以自己定义具体的实现,比如第一个更改格式和显示画面。

abstract void  surfaceChanged(SurfaceHolder holder, int format, int width, int height) 

abstract void  surfaceCreated(SurfaceHolder holder) 

abstract void  surfaceDestroyed(SurfaceHolder holder) 

对于Surface相关的,Android底层还提供了GPU加速功能,所以一般实时性很强的应用中主要使用SurfaceView而不是直接从View构建,同时Android123未来后面说到的OpenGL中的GLSurfaceView也是从该类实现。

Android游戏开发之旅三 View类详解

在Android游戏开发之旅二中我们讲到了View和SurfaceView的区别,今天Android123从View类开始着重的介绍Android图形显示基类的相关方法和注意点。

自定义View的常用方法:

  • onFinishInflate() 当View中所有的子控件均被映射成xml后触发
  • onMeasure(int, int) 确定所有子元素的大小
  • onLayout(boolean, int, int, int, int) 当View分配所有的子元素的大小和位置时触发
  • onSizeChanged(int, int, int, int) 当view的大小发生变化时触发
  • onDraw(Canvas) view渲染内容的细节
  • onKeyDown(int, KeyEvent) 有按键按下后触发
  • onKeyUp(int, KeyEvent) 有按键按下后弹起时触发
  • onTrackballEvent(MotionEvent) 轨迹球事件
  • onTouchEvent(MotionEvent) 触屏事件
  • onFocusChanged(boolean, int, Rect) 当View获取或失去焦点时触发
  • onWindowFocusChanged(boolean) 当窗口包含的view获取或失去焦点时触发
  • onAttachedToWindow() 当view被附着到一个窗口时触发
  • onDetachedFromWindow() 当view离开附着的窗口时触发,Android123提示该方法和 onAttachedToWindow() 是相反的。
  • onWindowVisibilityChanged(int) 当窗口中包含的可见的view发生变化时触发

以上是View实现的一些基本接口的回调方法,一般我们需要处理画布的显示时,重写onDraw(Canvas)用的的是最多的:

  @Override
 
   protected void onDraw(Canvas canvas) {
 
    //这里我们直接使用canvas对象处理当前的画布,比如说使用Paint来选择要填充的颜色
 
   Paint paintBackground = new Paint();
 
   paintBackground.setColor(getResources().getColor(R.color.xxx));  //从Res中找到名为xxx的color颜色定义
 
   canvas.drawRect(0, 0, getWidth(), getHeight(), paintBackground); //设置当前画布的背景颜色为paintBackground中定义的颜色,以0,0作为为起点,以当前画布的宽度和高度为重点即整块画布来填充。   
 
具体的请查看Android123未来讲到的CanvasPaint,在Canvas中我们可以实现画路径,图形,区域,线。而Paint作为绘画方式的对象可以设置颜色,大小,甚至字体的类型等等。
 
}

当然还有就是处理窗口还原状态问题(一般用于横竖屏切换),除了在Activity中可以调用外,开发游戏时我们尽量在View中使用类似

@Override
 
   protected Parcelable onSaveInstanceState() { 
 
      Parcelable p = super.onSaveInstanceState();
 
      Bundle bundle = new Bundle();
 
      bundle.putInt("x", pX);
 
      bundle.putInt("y", pY);
 
      bundle.putParcelable("android123_state", p);
 
      return bundle;
 
   }
 
   @Override
 
   protected void onRestoreInstanceState(Parcelable state) { 
 
      Bundle bundle = (Bundle) state;
 
      dosomething(bundle.getInt("x"), bundle.getInt("y")); //获取刚才存储的x和y信息
 
      super.onRestoreInstanceState(bundle.getParcelable("android123_state"));
 
      return;
 
   }

在View中如果需要强制调用绘制方法onDraw,可以使用invalidate()方法,它有很多重载版本,同时在线程中的postInvailidate()方法将在Android游戏开发之旅六中的 自定义View完整篇讲到。

Android游戏开发之旅四 Canvas和Paint实例

昨天我们在Android游戏开发之旅三 View详解中提到了onDraw方法,有关详细的实现我们今天主要说下Android的Canvas和Paint对象的使用实例。

Canvas类主要实现了屏幕的绘制过程,其中包含了很多实用的方法,比如绘制一条路径、区域、贴图、画点、画线、渲染文本,下面是Canvas类常用的方法,当然Android开发网提示大家很多方法有不同的重载版本,参数更灵活。

    void drawRect(RectF rect, Paint paint) //绘制区域,参数一为RectF一个区域
    void drawPath(Path path, Paint paint) //绘制一个路径,参数一为Path路径对象 
    void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)   //贴图,参数一就是我们常规的Bitmap对象,参数二是源区域(Android123提示这里是
                                                                               //bitmap),参数三是目标区域(应该在canvas的位置和大小),参数四是Paint画刷对象,
                                                                               //因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。 
    void drawLine(float startX, float startY, float stopX, float stopY, Paint paint)  //画线,参数一起始点的x轴位置,参数二起始点的y轴位置,
                                                                                                   //参数三终点的x轴水平位置,参数四y轴垂直位置,最后一个参数为Paint画刷对象。
    void drawPoint(float x, float y, Paint paint) //画点,参数一水平x轴,参数二垂直y轴,第三个参数为Paint对象。
    void drawText(String text, float x, float y, Paint paint)  //渲染文本,Canvas类除了上面的还可以描绘文字,
                                                                        //参数一是String类型的文本,参数二x轴,参数三y轴,参数四是Paint对象。
    void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) //在路径上绘制文本,相对于上面第二个参数是Path路径对象


从上面来看我们可以看出Canvas绘制类比较简单同时很灵活,实现一般的方法通常没有问题,同时可以叠加的处理设计出一些效果,不过细心的网友可能发现最后一个参数均为Paint对象。如果我们把Canvas当做绘画师来看,那么Paint就是我们绘画的工具,比如画笔、画刷、颜料等等。

Paint类常用方法:

void  setARGB(int a, int r, int g, int b)  设置Paint对象颜色,参数一为alpha透明通道
void  setAlpha(int a)  设置alpha不透明度,范围为0~255
void  setAntiAlias(boolean aa)  //是否抗锯齿
void  setColor(int color)  //设置颜色,这里Android内部定义的有Color类包含了一些常见颜色定义. 
void  setFakeBoldText(boolean fakeBoldText)  //设置伪粗体文本
void  setLinearText(boolean linearText)  //设置线性文本
PathEffect  setPathEffect(PathEffect effect)  //设置路径效果
Rasterizer  setRasterizer(Rasterizer rasterizer) //设置光栅化
Shader  setShader(Shader shader)  //设置阴影 
void  setTextAlign(Paint.Align align)  //设置文本对齐
void  setTextScaleX(float scaleX)  //设置文本缩放倍数,1.0f为原始
void  setTextSize(float textSize)  //设置字体大小
Typeface  setTypeface(Typeface typeface)  //设置字体,Typeface包含了字体的类型,粗细,还有倾斜、颜色等。
void  setUnderlineText(boolean underlineText)  //设置下划线

最终Canvas和Paint在onDraw中直接使用

@Override
   protected void onDraw(Canvas canvas) {
    Paint paintRed=new Paint();
    paintRed.setColor(Color.Red);
    canvas.drawPoint(11,3,paintRed); //在坐标11,3上画一个红点
  }

下一次将会具体讲到强大的Path路径,和字体Typeface相关的使用。

Android游戏开发之旅五 Path和Typeface

今天我们继续处理上次 Android游戏开发之旅四 Canvas和Paint实例中提到的Path路径和Typeface字体两个类。对于Android游戏开发或者说2D绘图中来讲Path路径可以用强大这个词来形容。在Photoshop中我们可能还记得使用钢笔工具绘制路径的方法。Path路径类在位于android.graphics.Path中,Path的构造方法比较简单,如下

Path cwj=new Path(); //构造方法

下面我们画一个封闭的原型路径,我们使用Path类的addCircle方法

    cwj.addCircle(10,10,50,Direction.CW); //参数一为x轴水平位置,参数二为y轴垂直位置,第三个参数为圆形的半径,最后是绘制的方向,CW为顺时针方向,而CCW是逆时针方向。
    结合Android上次提到的Canvas类中的绘制方法drawPath和drawTextOnPath,我们继续可以在onDraw中加入。
   canvas.drawPath(cwj,paintPath); //Android123提示大家这里paintPath为路径的画刷颜色,可以见下文完整的源代码。
   canvas.drawTextOnPath("Android123 - CWJ",cwj,0,15,paintText); //将文字绘制到路径中去,有关drawTextOnPath的参数如下:
    方法原型public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint) 
    参数列表
text  为需要在路径上绘制的文字内容。 
path 将文字绘制到哪个路径。  
hOffset   距离路径开始的距离
vOffset   离路径的上下高度,这里Android开发网提示大家,该参数类型为float浮点型,除了精度为8位小数外,可以为正或负,当为正时文字在路径的圈里面,为负时在路径的圈外面。 
paint  最后仍然是一个Paint对象用于制定Text本文的颜色、字体、大小等属性。

下面是我们的onDraw方法中如何绘制路径的演示代码为:

@Override
 
   protected void onDraw(Canvas canvas) {
 
    Paint paintPath=new Paint();
 
    Paint paintText=new Paint();
 
    paintPath.setColor(Color.Red); //路径的画刷为红色
 
    paintText.setColor(Color.Blue); //路径上的文字为蓝色
 
    Path pathCWJ=new Path();
 
    pathCWJ.addCircle(10,10,50,Direction.CW); // 半径为50px,绘制的方向CW为顺时针
 
    canvas.drawPath(pathCWJ,paintPath); 
 
   canvas.drawTextOnPath("Android123 - CWJ",pathCWJ,0,15,paintText); //在路径上绘制文字
 
  }

有关路径类常用的方法如下:

void  addArc(RectF oval, float startAngle, float sweepAngle)  //为路径添加一个多边形

void  addCircle(float x, float y, float radius, Path.Direction dir)  //给path添加圆圈

void  addOval(RectF oval, Path.Direction dir)  //添加椭圆形

void  addRect(RectF rect, Path.Direction dir)  //添加一个区域

void  addRoundRect(RectF rect, float[] radii, Path.Direction dir)  //添加一个圆角区域

boolean  isEmpty()  //判断路径是否为空

void  transform(Matrix matrix)  //应用矩阵变换

void  transform(Matrix matrix, Path dst)  //应用矩阵变换并将结果放到新的路径中,即第二个参数。


有关路径的高级效果大家可以使用PathEffect类,有关路径的更多实例Android123将在今后的游戏开发实战中讲解道。


Typeface字体类

平时我们在TextView中需要设置显示的字体可以通过TextView中的setTypeface方法来指定一个Typeface对象,因为Android的字体类比较简单,我们列出所有成员方法

static Typeface  create(Typeface family, int style)  //静态方法,参数一为字体类型这里是Typeface的静态定义,如宋体,参数二风格,如粗体,斜体

static Typeface  create(String familyName, int style)  //静态方法,参数一为字体名的字符串,参数二为风格同上,这里我们推荐使用上面的方法。

static Typeface  createFromAsset(AssetManager mgr, String path)  //静态方法,参数一为AssetManager对象,主要用于从APK的assets文件夹中取出字体,参数二为相对于Android工程下的assets文件夹中的外挂字体文件的路径。

static Typeface  createFromFile(File path)  //静态方法,从文件系统构造一个字体,这里参数可以是sdcard中的某个字体文件

static Typeface  createFromFile(String path)  //静态方法,从指定路径中构造字体

static Typeface  defaultFromStyle(int style) //静态方法,返回默认的字体风格

int  getStyle()  //获取当前字体风格

final boolean  isBold()  //判断当前是否为粗体

final boolean  isItalic()  //判断当前风格是否为斜体

本类的常量静态定义,首先为字体类型名称

Typeface DEFAULT  
Typeface DEFAULT_BOLD
Typeface MONOSPACE
Typeface SANS_SERIF
Typeface SERIF

字体风格名称

int BOLD  
int BOLD_ITALIC  
int ITALIC
int NORMAL 

明天我们将在 Android游戏开发之旅六 自定义View 一文中具体讲解onDraw以及什么时候会触发绘制方法,来实现我们自定义或子类化控件。

Android游戏开发之旅六 自定义View

有关Android的自定义View的框架今天我们一起讨论下,对于常规的游戏,我们在View中需要处理以下几种问题: 1.控制事件 2.刷新View 3. 绘制View

1. 对于控制事件今天我们只处理按键事件onKeyDown,以后的文章中将会讲到屏幕触控的具体处理onTouchEvent以及Sensor重力感应等方法。

2. 刷新view的方法这里主要有invalidate(int l, int t, int r, int b) 刷新局部,四个参数分别为左、上、右、下。整个view刷新 invalidate(),刷新一个矩形区域 invalidate(Rect dirty) ,刷新一个特性Drawable, invalidateDrawable(Drawable drawable) ,执行invalidate类的方法将会设置view为无效,最终导致onDraw方法被重新调用。由于今天的view比较简单,Android123提示大家如果在线程中刷新,除了使用handler方式外,可以在Thread中直接使用postInvalidate方法来实现。

3. 绘制View主要是onDraw()中通过形参canvas来处理,相关的绘制主要有drawRect、drawLine、drawPath等等。view方法内部还重写了很多接口,其回调方法可以帮助我们判断出view的位置和大小,比如onMeasure(int, int) Called to determine the size requirements for this view and all of its children. 、onLayout(boolean, int, int, int, int) Called when this view should assign a size and position to all of its children 和onSizeChanged(int, int, int, int) Called when the size of this view has changed. 具体的作用,大家可以用Logcat获取当view变化时每个形参的变动。

下面cwjView是我们为今后游戏设计的一个简单自定义View框架,我们可以看到在Android平台自定义view还是很简单的,同时Java支持多继承可以帮助我们不断的完善复杂的问题。


public class cwjView extends View {
 
    public cwjView(Context context) {
 
 
 
      super(context); 
 
 
 
      setFocusable(true); //允许获得焦点
 
      setFocusableInTouchMode(true); //获取焦点时允许触控
 
 
 
   }
 
   @Override
 
   protected Parcelable onSaveInstanceState() {  //处理窗口保存事件
 
      Parcelable pSaved = super.onSaveInstanceState();
 
 
 
      Bundle bundle = new Bundle();
 
 
 
     //dosomething
 
      return bundle;
 
   }
 
   @Override
 
   protected void onRestoreInstanceState(Parcelable state) {  //处理窗口还原事件
 
 
 
      Bundle bundle = (Bundle) state;
 
     //dosomething
 
     super.onRestoreInstanceState(bundle.getParcelable("cwj"));
 
      return;
 
   }
 
       @Override
 
   protected void onSizeChanged(int w, int h, int oldw, int oldh) //处理窗口大小变化事件
 
   {
 
      super.onSizeChanged(w, h, oldw, oldh);
 
   }
 
   @Override
 
   protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)  
 
   {
 
      super.onMeasure(widthMeasureSpec, heightMeasureSpec); //如果不让父类处理记住调用setMeasuredDimension
 
   }
 
 
 
   @Override
 
   protected void onLayout (boolean changed, int left, int top, int right, int bottom) 
 
   {
 
    super.onLayout (changed,left,top, ight,bottom) ;
 
   }
 
   @Override
 
   protected void onDraw(Canvas canvas) {
 
 
 
      Paint bg = new Paint();
 
      bg.setColor(Color.Red);
 
      canvas.drawRect(0, 0, getWidth()/2, getHeight()/2, bg); //将view的左上角四分之一填充为红色  
 
 
 
   }
 
   @Override
 
   public boolean onTouchEvent(MotionEvent event) {
 
 
 
         return super.onTouchEvent(event); //让父类处理屏幕触控事件
 
 
 
   }
 
   @Override
 
   public boolean onKeyDown(int keyCode, KeyEvent event) { //处理按键事件,响应的轨迹球事件为 public boolean onTrackballEvent (MotionEvent event) 
 
 
 
      switch (keyCode) {
 
      case KeyEvent.KEYCODE_DPAD_UP:
 
 
 
         break;
 
      case KeyEvent.KEYCODE_DPAD_DOWN:
 
 
 
         break;
 
      case KeyEvent.KEYCODE_DPAD_LEFT:
 
 
 
         break;
 
      case KeyEvent.KEYCODE_DPAD_RIGHT:
 
 
 
         break;
 
 
 
      case KeyEvent.KEYCODE_DPAD_CENTER: //处理中键按下
 
 
 
         break;
 
      default:
 
         return super.onKeyDown(keyCode, event);
 
      }
 
      return true;
 
   }
 
}

上面我们可以看到onMeasure使用的是父类的处理方法,如果我们需要解决自定义View的大小,可以尝试下面的方法

@Override
 
   protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)  
 
   {
 
      height = View.MeasureSpec.getSize(heightMeasureSpec);  
 
      width = View.MeasureSpec.getSize(widthMeasureSpec);  
 
      setMeasuredDimension(width,height);   //这里面是原始的大小,需要重新计算可以修改本行
 
     //dosomething
 
   }

Android游戏开发之旅七 自定义SurfaceView

今天我们说下未来的Android游戏引擎模板架构问题,对于游戏我们还是选择SurfaceView,相关的原因Android123已经在Android游戏开发之旅二 View和SurfaceView中说的很清楚了,这里我们直接继承SurfaceView,实现SurfaceHolder.Callback接口,处理surfaceCreated、surfaceChanged以及surfaceDestroyed方法,这里我们并没有把按键控制传入,最终游戏的控制方面仍然由View内部类处理比较好,有关SurfaceView的具体我们可以参见Android开源项目的Camera中有关画面捕捉以及VideoView的控件实现大家可以清晰了解最终的用意。

public class cwjView extends SurfaceView implements SurfaceHolder.Callback {
 
public cwjView(Context context, AttributeSet attrs) {
 
  super(context, attrs);
 
  SurfaceHolder holder=getHolder();
 
  holder.addCallback(this);  
 
  setFocusable(true);
 
}
 
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
 
}
 
public void surfaceCreated(SurfaceHolder holder) {
 
}
 
public void surfaceDestroyed(SurfaceHolder holder) {
 
}
 
@Override
 
public void onWindowFocusChanged(boolean hasWindowFocus) {
 
 
}
 
 
}

Android游戏开发之旅八 SurfaceView类实例

有关SurfaceView我们将通过三个系统自带的例子来深入掌握Android绘图必会的SurfaceView,今天我们以SDK中的Sample游戏lunarlander中的LunarView具体实现,Android123建议大家导入该游戏工程到你的Eclipse然后自己编译先玩一下这个游戏,然后再看代码比较好理解。


class LunarView extends SurfaceView implements SurfaceHolder.Callback {
 
    class LunarThread extends Thread {
 
        /*
 
         * Difficulty setting constants
 
         */
 
        public static final int DIFFICULTY_EASY = 0;
 
        public static final int DIFFICULTY_HARD = 1;
 
        public static final int DIFFICULTY_MEDIUM = 2;
 
        /*
 
         * Physics constants
 
         */
 
        public static final int PHYS_DOWN_ACCEL_SEC = 35;
 
        public static final int PHYS_FIRE_ACCEL_SEC = 80;
 
        public static final int PHYS_FUEL_INIT = 60;
 
        public static final int PHYS_FUEL_MAX = 100;
 
        public static final int PHYS_FUEL_SEC = 10;
 
        public static final int PHYS_SLEW_SEC = 120; // degrees/second rotate
 
        public static final int PHYS_SPEED_HYPERSPACE = 180;
 
        public static final int PHYS_SPEED_INIT = 30;
 
        public static final int PHYS_SPEED_MAX = 120;
 
        /*
 
         * State-tracking constants
 
         */
 
        public static final int STATE_LOSE = 1;
 
        public static final int STATE_PAUSE = 2;
 
        public static final int STATE_READY = 3;
 
        public static final int STATE_RUNNING = 4;
 
        public static final int STATE_WIN = 5;
 
        /*
 
         * Goal condition constants
 
         */
 
        public static final int TARGET_ANGLE = 18; // > this angle means crash
 
        public static final int TARGET_BOTTOM_PADDING = 17; // px below gear
 
        public static final int TARGET_PAD_HEIGHT = 8; // how high above ground
 
        public static final int TARGET_SPEED = 28; // > this speed means crash
 
        public static final double TARGET_WIDTH = 1.6; // width of target
 
        /*
 
         * UI constants (i.e. the speed & fuel bars)
 
         */
 
        public static final int UI_BAR = 100; // width of the bar(s)
 
        public static final int UI_BAR_HEIGHT = 10; // height of the bar(s)
 
        private static final String KEY_DIFFICULTY = "mDifficulty";
 
        private static final String KEY_DX = "mDX";
 
        private static final String KEY_DY = "mDY";
 
        private static final String KEY_FUEL = "mFuel";
 
        private static final String KEY_GOAL_ANGLE = "mGoalAngle";
 
        private static final String KEY_GOAL_SPEED = "mGoalSpeed";
 
        private static final String KEY_GOAL_WIDTH = "mGoalWidth";
 
        private static final String KEY_GOAL_X = "mGoalX";
 
        private static final String KEY_HEADING = "mHeading";
 
        private static final String KEY_LANDER_HEIGHT = "mLanderHeight";
 
        private static final String KEY_LANDER_WIDTH = "mLanderWidth";
 
        private static final String KEY_WINS = "mWinsInARow";
 
        private static final String KEY_X = "mX";
 
        private static final String KEY_Y = "mY";
 
        /*
 
         * Member (state) fields
 
         */
 
        /** The drawable to use as the background of the animation canvas */
 
        private Bitmap mBackgroundImage;
 
        /**
 
         * Current height of the surface/canvas.
 
         * 
 
         * @see #setSurfaceSize
 
         */
 
        private int mCanvasHeight = 1;
 
        /**
 
         * Current width of the surface/canvas.
 
         * 
 
         * @see #setSurfaceSize
 
         */
 
        private int mCanvasWidth = 1;
 
        /** What to draw for the Lander when it has crashed */
 
        private Drawable mCrashedImage;
 
        /**
 
         * Current difficulty -- amount of fuel, allowed angle, etc. Default is
 
         * MEDIUM.
 
         */
 
        private int mDifficulty;
 
        /** Velocity dx. */
 
        private double mDX;
 
        /** Velocity dy. */
 
        private double mDY;
 
        /** Is the engine burning? */
 
        private boolean mEngineFiring;
 
        /** What to draw for the Lander when the engine is firing */
 
        private Drawable mFiringImage;
 
        /** Fuel remaining */
 
        private double mFuel;
 
        /** Allowed angle. */
 
        private int mGoalAngle;
 
        /** Allowed speed. */
 
        private int mGoalSpeed;
 
        /** Width of the landing pad. */
 
        private int mGoalWidth;
 
        /** X of the landing pad. */
 
        private int mGoalX;
 
        /** Message handler used by thread to interact with TextView */
 
        private Handler mHandler;
 
        /**
 
         * Lander heading in degrees, with 0 up, 90 right. Kept in the range
 
         * 0..360.
 
         */
 
        private double mHeading;
 
        /** Pixel height of lander image. */
 
        private int mLanderHeight;
 
        /** What to draw for the Lander in its normal state */
 
        private Drawable mLanderImage;
 
        /** Pixel width of lander image. */
 
        private int mLanderWidth;
 
        /** Used to figure out elapsed time between frames */
 
        private long mLastTime;
 
        /** Paint to draw the lines on screen. */
 
        private Paint mLinePaint;
 
        /** "Bad" speed-too-high variant of the line color. */
 
        private Paint mLinePaintBad;
 
        /** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */
 
        private int mMode;
 
        /** Currently rotating, -1 left, 0 none, 1 right. */
 
        private int mRotating;
 
        /** Indicate whether the surface has been created & is ready to draw */
 
        private boolean mRun = false;
 
        /** Scratch rect object. */
 
        private RectF mScratchRect;
 
        /** Handle to the surface manager object we interact with */
 
        private SurfaceHolder mSurfaceHolder;
 
        /** Number of wins in a row. */
 
        private int mWinsInARow;
 
        /** X of lander center. */
 
        private double mX;
 
        /** Y of lander center. */
 
        private double mY;
 
        public LunarThread(SurfaceHolder surfaceHolder, Context context,
 
                Handler handler) {
 
            // get handles to some important objects
 
            mSurfaceHolder = surfaceHolder;
 
            mHandler = handler;
 
            mContext = context;
 
            Resources res = context.getResources();
 
            // cache handles to our key sprites & other drawables
 
            mLanderImage = context.getResources().getDrawable(
 
                    R.drawable.lander_plain);
 
            mFiringImage = context.getResources().getDrawable(
 
                    R.drawable.lander_firing);
 
            mCrashedImage = context.getResources().getDrawable(
 
                    R.drawable.lander_crashed);
 
            // load background image as a Bitmap instead of a Drawable b/c
 
            // we don't need to transform it and it's faster to draw this way
 
            mBackgroundImage = BitmapFactory.decodeResource(res,
 
                    R.drawable.earthrise);
 
            // Use the regular lander image as the model size for all sprites
 
            mLanderWidth = mLanderImage.getIntrinsicWidth();
 
            mLanderHeight = mLanderImage.getIntrinsicHeight();
 
            // Initialize paints for speedometer
 
            mLinePaint = new Paint();
 
            mLinePaint.setAntiAlias(true);
 
            mLinePaint.setARGB(255, 0, 255, 0);
 
            mLinePaintBad = new Paint();
 
            mLinePaintBad.setAntiAlias(true);
 
            mLinePaintBad.setARGB(255, 120, 180, 0);
 
            mScratchRect = new RectF(0, 0, 0, 0);
 
            mWinsInARow = 0;
 
            mDifficulty = DIFFICULTY_MEDIUM;
 
            // initial show-up of lander (not yet playing)
 
            mX = mLanderWidth;
 
            mY = mLanderHeight * 2;
 
            mFuel = PHYS_FUEL_INIT;
 
            mDX = 0;
 
            mDY = 0;
 
            mHeading = 0;
 
            mEngineFiring = true;
 
        }
 
        /**
 
         * Starts the game, setting parameters for the current difficulty.
 
         */
 
        public void doStart() {
 
            synchronized (mSurfaceHolder) {
 
                // First set the game for Medium difficulty
 
                mFuel = PHYS_FUEL_INIT;
 
                mEngineFiring = false;
 
                mGoalWidth = (int) (mLanderWidth * TARGET_WIDTH);
 
                mGoalSpeed = TARGET_SPEED;
 
                mGoalAngle = TARGET_ANGLE;
 
                int speedInit = PHYS_SPEED_INIT;
 
                // Adjust difficulty params for EASY/HARD
 
                if (mDifficulty == DIFFICULTY_EASY) {
 
                    mFuel = mFuel * 3 / 2;
 
                    mGoalWidth = mGoalWidth * 4 / 3;
 
                    mGoalSpeed = mGoalSpeed * 3 / 2;
 
                    mGoalAngle = mGoalAngle * 4 / 3;
 
                    speedInit = speedInit * 3 / 4;
 
                } else if (mDifficulty == DIFFICULTY_HARD) {
 
                    mFuel = mFuel * 7 / 8;
 
                    mGoalWidth = mGoalWidth * 3 / 4;
 
                    mGoalSpeed = mGoalSpeed * 7 / 8;
 
                    speedInit = speedInit * 4 / 3;
 
                }
 
                // pick a convenient initial location for the lander sprite
 
                mX = mCanvasWidth / 2;
 
                mY = mCanvasHeight - mLanderHeight / 2;
 
                // start with a little random motion
 
                mDY = Math.random() * -speedInit;
 
                mDX = Math.random() * 2 * speedInit - speedInit;
 
                mHeading = 0;
 
                // Figure initial spot for landing, not too near center
 
                while (true) {
 
                    mGoalX = (int) (Math.random() * (mCanvasWidth - mGoalWidth));
 
                    if (Math.abs(mGoalX - (mX - mLanderWidth / 2)) > mCanvasHeight / 6)
 
                        break;
 
                }
 
                mLastTime = System.currentTimeMillis() + 100;
 
                setState(STATE_RUNNING);
 
            }
 
        }
 
        /**
 
         * Pauses the physics update & animation.
 
         */
 
        public void pause() {
 
            synchronized (mSurfaceHolder) {
 
                if (mMode == STATE_RUNNING) setState(STATE_PAUSE);
 
            }
 
        }
 
        /**
 
         * Restores game state from the indicated Bundle. Typically called when
 
         * the Activity is being restored after having been previously
 
         * destroyed.
 
         * 
 
         * @param savedState Bundle containing the game state
 
         */
 
        public synchronized void restoreState(Bundle savedState) {
 
            synchronized (mSurfaceHolder) {
 
                setState(STATE_PAUSE);
 
                mRotating = 0;
 
                mEngineFiring = false;
 
                mDifficulty = savedState.getInt(KEY_DIFFICULTY);
 
                mX = savedState.getDouble(KEY_X);
 
                mY = savedState.getDouble(KEY_Y);
 
                mDX = savedState.getDouble(KEY_DX);
 
                mDY = savedState.getDouble(KEY_DY);
 
                mHeading = savedState.getDouble(KEY_HEADING);
 
                mLanderWidth = savedState.getInt(KEY_LANDER_WIDTH);
 
                mLanderHeight = savedState.getInt(KEY_LANDER_HEIGHT);
 
                mGoalX = savedState.getInt(KEY_GOAL_X);
 
                mGoalSpeed = savedState.getInt(KEY_GOAL_SPEED);
 
                mGoalAngle = savedState.getInt(KEY_GOAL_ANGLE);
 
                mGoalWidth = savedState.getInt(KEY_GOAL_WIDTH);
 
                mWinsInARow = savedState.getInt(KEY_WINS);
 
                mFuel = savedState.getDouble(KEY_FUEL);
 
            }
 
        }
 
        @Override
 
        public void run() {
 
            while (mRun) {
 
                Canvas c = null;
 
                try {
 
                    c = mSurfaceHolder.lockCanvas(null);
 
                    synchronized (mSurfaceHolder) {
 
                        if (mMode == STATE_RUNNING) updatePhysics();
 
                        doDraw(c);
 
                    }
 
                } finally {
 
                    // do this in a finally so that if an exception is thrown
 
                    // during the above, we don't leave the Surface in an
 
                    // inconsistent state
 
                    if (c != null) {
 
                        mSurfaceHolder.unlockCanvasAndPost(c);
 
                    }
 
                }
 
            }
 
        }
 
        /**
 
         * Dump game state to the provided Bundle. Typically called when the
 
         * Activity is being suspended.
 
         * 
 
         * @return Bundle with this view's state
 
         */
 
        public Bundle saveState(Bundle map) {
 
            synchronized (mSurfaceHolder) {
 
                if (map != null) {
 
                    map.putInt(KEY_DIFFICULTY, Integer.valueOf(mDifficulty));
 
                    map.putDouble(KEY_X, Double.valueOf(mX));
 
                    map.putDouble(KEY_Y, Double.valueOf(mY));
 
                    map.putDouble(KEY_DX, Double.valueOf(mDX));
 
                    map.putDouble(KEY_DY, Double.valueOf(mDY));
 
                    map.putDouble(KEY_HEADING, Double.valueOf(mHeading));
 
                    map.putInt(KEY_LANDER_WIDTH, Integer.valueOf(mLanderWidth));
 
                    map.putInt(KEY_LANDER_HEIGHT, Integer
 
                            .valueOf(mLanderHeight));
 
                    map.putInt(KEY_GOAL_X, Integer.valueOf(mGoalX));
 
                    map.putInt(KEY_GOAL_SPEED, Integer.valueOf(mGoalSpeed));
 
                    map.putInt(KEY_GOAL_ANGLE, Integer.valueOf(mGoalAngle));
 
                    map.putInt(KEY_GOAL_WIDTH, Integer.valueOf(mGoalWidth));
 
                    map.putInt(KEY_WINS, Integer.valueOf(mWinsInARow));
 
                    map.putDouble(KEY_FUEL, Double.valueOf(mFuel));
 
                }
 
            }
 
            return map;
 
        }
 
        /**
 
         * Sets the current difficulty.
 
         * 
 
         * @param difficulty
 
         */
 
        public void setDifficulty(int difficulty) {
 
            synchronized (mSurfaceHolder) {
 
                mDifficulty = difficulty;
 
            }
 
        }
 
        /**
 
         * Sets if the engine is currently firing.
 
         */
 
        public void setFiring(boolean firing) {
 
            synchronized (mSurfaceHolder) {
 
                mEngineFiring = firing;
 
            }
 
        }
 
        /**
 
         * Used to signal the thread whether it should be running or not.
 
         * Passing true allows the thread to run; passing false will shut it
 
         * down if it's already running. Calling start() after this was most
 
         * recently called with false will result in an immediate shutdown.
 
         * 
 
         * @param b true to run, false to shut down
 
         */
 
        public void setRunning(boolean b) {
 
            mRun = b;
 
        }
 
        /**
 
         * Sets the game mode. That is, whether we are running, paused, in the
 
         * failure state, in the victory state, etc.
 
         * 
 
         * @see #setState(int, CharSequence)
 
         * @param mode one of the STATE_* constants
 
         */
 
        public void setState(int mode) {
 
            synchronized (mSurfaceHolder) {
 
                setState(mode, null);
 
            }
 
        }
 
        /**
 
         * Sets the game mode. That is, whether we are running, paused, in the
 
         * failure state, in the victory state, etc.
 
         * 
 
         * @param mode one of the STATE_* constants
 
         * @param message string to add to screen or null
 
         */
 
        public void setState(int mode, CharSequence message) {
 
            /*
 
             * This method optionally can cause a text message to be displayed
 
             * to the user when the mode changes. Since the View that actually
 
             * renders that text is part of the main View hierarchy and not
 
             * owned by this thread, we can't touch the state of that View.
 
             * Instead we use a Message + Handler to relay commands to the main
 
             * thread, which updates the user-text View.
 
             */
 
            synchronized (mSurfaceHolder) {
 
                mMode = mode;
 
                if (mMode == STATE_RUNNING) {
 
                    Message msg = mHandler.obtainMessage();
 
                    Bundle b = new Bundle();
 
                    b.putString("text", "");
 
                    b.putInt("viz", View.INVISIBLE);
 
                    msg.setData(b);
 
                    mHandler.sendMessage(msg);
 
                } else {
 
                    mRotating = 0;
 
                    mEngineFiring = false;
 
                    Resources res = mContext.getResources();
 
                    CharSequence str = "";
 
                    if (mMode == STATE_READY)
 
                        str = res.getText(R.string.mode_ready);
 
                    else if (mMode == STATE_PAUSE)
 
                        str = res.getText(R.string.mode_pause);
 
                    else if (mMode == STATE_LOSE)
 
                        str = res.getText(R.string.mode_lose);
 
                    else if (mMode == STATE_WIN)
 
                        str = res.getString(R.string.mode_win_prefix)
 
                                + mWinsInARow + " "
 
                                + res.getString(R.string.mode_win_suffix);
 
                    if (message != null) {
 
                        str = message + "\n" + str;
 
                    }
 
                    if (mMode == STATE_LOSE) mWinsInARow = 0;
 
                    Message msg = mHandler.obtainMessage();
 
                    Bundle b = new Bundle();
 
                    b.putString("text", str.toString());
 
                    b.putInt("viz", View.VISIBLE);
 
                    msg.setData(b);
 
                    mHandler.sendMessage(msg);
 
                }
 
            }
 
        }
 
        /* Callback invoked when the surface dimensions change. */
 
        public void setSurfaceSize(int width, int height) {
 
            // synchronized to make sure these all change atomically
 
            synchronized (mSurfaceHolder) {
 
                mCanvasWidth = width;
 
                mCanvasHeight = height;
 
                // don't forget to resize the background image
 
                mBackgroundImage = mBackgroundImage.createScaledBitmap(
 
                        mBackgroundImage, width, height, true);
 
            }
 
        }
 
        /**
 
         * Resumes from a pause.
 
         */
 
        public void unpause() {
 
            // Move the real time clock up to now
 
            synchronized (mSurfaceHolder) {
 
                mLastTime = System.currentTimeMillis() + 100;
 
            }
 
            setState(STATE_RUNNING);
 
        }
 
        /**
 
         * Handles a key-down event.
 
         * 
 
         * @param keyCode the key that was pressed
 
         * @param msg the original event object
 
         * @return true
 
         */
 
        boolean doKeyDown(int keyCode, KeyEvent msg) {
 
            synchronized (mSurfaceHolder) {
 
                boolean okStart = false;
 
                if (keyCode == KeyEvent.KEYCODE_DPAD_UP) okStart = true;
 
                if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) okStart = true;
 
                if (keyCode == KeyEvent.KEYCODE_S) okStart = true;
 
                boolean center = (keyCode == KeyEvent.KEYCODE_DPAD_UP);
 
                if (okStart
 
                        && (mMode == STATE_READY || mMode == STATE_LOSE || mMode == STATE_WIN)) {
 
                    // ready-to-start -> start
 
                    doStart();
 
                    return true;
 
                } else if (mMode == STATE_PAUSE && okStart) {
 
                    // paused -> running
 
                    unpause();
 
                    return true;
 
                } else if (mMode == STATE_RUNNING) {
 
                    // center/space -> fire
 
                    if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
 
                            || keyCode == KeyEvent.KEYCODE_SPACE) {
 
                        setFiring(true);
 
                        return true;
 
                        // left/q -> left
 
                    } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
 
                            || keyCode == KeyEvent.KEYCODE_Q) {
 
                        mRotating = -1;
 
                        return true;
 
                        // right/w -> right
 
                    } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
 
                            || keyCode == KeyEvent.KEYCODE_W) {
 
                        mRotating = 1;
 
                        return true;
 
                        // up -> pause
 
                    } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
 
                        pause();
 
                        return true;
 
                    }
 
                }
 
                return false;
 
            }
 
        }
 
        /**
 
         * Handles a key-up event.
 
         * 
 
         * @param keyCode the key that was pressed
 
         * @param msg the original event object
 
         * @return true if the key was handled and consumed, or else false
 
         */
 
        boolean doKeyUp(int keyCode, KeyEvent msg) {
 
            boolean handled = false;
 
            synchronized (mSurfaceHolder) {
 
                if (mMode == STATE_RUNNING) {
 
                    if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
 
                            || keyCode == KeyEvent.KEYCODE_SPACE) {
 
                        setFiring(false);
 
                        handled = true;
 
                    } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
 
                            || keyCode == KeyEvent.KEYCODE_Q
 
                            || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
 
                            || keyCode == KeyEvent.KEYCODE_W) {
 
                        mRotating = 0;
 
                        handled = true;
 
                    }
 
                }
 
            }
 
            return handled;
 
        }
 
        /**
 
         * Draws the ship, fuel/speed bars, and background to the provided
 
         * Canvas.
 
         */
 
        private void doDraw(Canvas canvas) {
 
            // Draw the background image. Operations on the Canvas accumulate
 
            // so this is like clearing the screen.
 
            canvas.drawBitmap(mBackgroundImage, 0, 0, null);
 
            int yTop = mCanvasHeight - ((int) mY + mLanderHeight / 2);
 
            int xLeft = (int) mX - mLanderWidth / 2;
 
            // Draw the fuel gauge
 
            int fuelWidth = (int) (UI_BAR * mFuel / PHYS_FUEL_MAX);
 
            mScratchRect.set(4, 4, 4 + fuelWidth, 4 + UI_BAR_HEIGHT);
 
            canvas.drawRect(mScratchRect, mLinePaint);
 
            // Draw the speed gauge, with a two-tone effect
 
            double speed = Math.sqrt(mDX * mDX + mDY * mDY);
 
            int speedWidth = (int) (UI_BAR * speed / PHYS_SPEED_MAX);
 
            if (speed <= mGoalSpeed) {
 
                mScratchRect.set(4 + UI_BAR + 4, 4,
 
                        4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT);
 
                canvas.drawRect(mScratchRect, mLinePaint);
 
            } else {
 
                // Draw the bad color in back, with the good color in front of
 
                // it
 
                mScratchRect.set(4 + UI_BAR + 4, 4,
 
                        4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT);
 
                canvas.drawRect(mScratchRect, mLinePaintBad);
 
                int goalWidth = (UI_BAR * mGoalSpeed / PHYS_SPEED_MAX);
 
                mScratchRect.set(4 + UI_BAR + 4, 4, 4 + UI_BAR + 4 + goalWidth,
 
                        4 + UI_BAR_HEIGHT);
 
                canvas.drawRect(mScratchRect, mLinePaint);
 
            }
 
            // Draw the landing pad
 
            canvas.drawLine(mGoalX, 1 + mCanvasHeight - TARGET_PAD_HEIGHT,
 
                    mGoalX + mGoalWidth, 1 + mCanvasHeight - TARGET_PAD_HEIGHT,
 
                    mLinePaint);
 
 
 
            // Draw the ship with its current rotation
 
            canvas.save();
 
            canvas.rotate((float) mHeading, (float) mX, mCanvasHeight
 
                    - (float) mY);
 
            if (mMode == STATE_LOSE) {
 
                mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
 
                        + mLanderHeight);
 
                mCrashedImage.draw(canvas);
 
            } else if (mEngineFiring) {
 
                mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
 
                        + mLanderHeight);
 
                mFiringImage.draw(canvas);
 
            } else {
 
                mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
 
                        + mLanderHeight);
 
                mLanderImage.draw(canvas);
 
            }
 
            canvas.restore();
 
        }
 
        /**
 
         * Figures the lander state (x, y, fuel, ...) based on the passage of
 
         * realtime. Does not invalidate(). Called at the start of draw().
 
         * Detects the end-of-game and sets the UI to the next state.
 
         */
 
        private void updatePhysics() {
 
            long now = System.currentTimeMillis();
 
            // Do nothing if mLastTime is in the future.
 
            // This allows the game-start to delay the start of the physics
 
            // by 100ms or whatever.
 
            if (mLastTime > now) return;
 
            double elapsed = (now - mLastTime) / 1000.0;
 
            // mRotating -- update heading
 
            if (mRotating != 0) {
 
                mHeading += mRotating * (PHYS_SLEW_SEC * elapsed);
 
                // Bring things back into the range 0..360
 
                if (mHeading < 0)
 
                    mHeading += 360;
 
                else if (mHeading >= 360) mHeading -= 360;
 
            }
 
            // Base accelerations -- 0 for x, gravity for y
 
            double ddx = 0.0;
 
            double ddy = -PHYS_DOWN_ACCEL_SEC * elapsed;
 
            if (mEngineFiring) {
 
                // taking 0 as up, 90 as to the right
 
                // cos(deg) is ddy component, sin(deg) is ddx component
 
                double elapsedFiring = elapsed;
 
                double fuelUsed = elapsedFiring * PHYS_FUEL_SEC;
 
                // tricky case where we run out of fuel partway through the
 
                // elapsed
 
                if (fuelUsed > mFuel) {
 
                    elapsedFiring = mFuel / fuelUsed * elapsed;
 
                    fuelUsed = mFuel;
 
                    // Oddball case where we adjust the "control" from here
 
                    mEngineFiring = false;
 
                }
 
                mFuel -= fuelUsed;
 
                // have this much acceleration from the engine
 
                double accel = PHYS_FIRE_ACCEL_SEC * elapsedFiring;
 
                double radians = 2 * Math.PI * mHeading / 360;
 
                ddx = Math.sin(radians) * accel;
 
                ddy += Math.cos(radians) * accel;
 
            }
 
            double dxOld = mDX;
 
            double dyOld = mDY;
 
            // figure speeds for the end of the period
 
            mDX += ddx;
 
            mDY += ddy;
 
            // figure position based on average speed during the period
 
            mX += elapsed * (mDX + dxOld) / 2;
 
            mY += elapsed * (mDY + dyOld) / 2;
 
            mLastTime = now;
 
            // Evaluate if we have landed ... stop the game
 
            double yLowerBound = TARGET_PAD_HEIGHT + mLanderHeight / 2
 
                    - TARGET_BOTTOM_PADDING;
 
            if (mY <= yLowerBound) {
 
                mY = yLowerBound;
 
                int result = STATE_LOSE;
 
                CharSequence message = "";
 
                Resources res = mContext.getResources();
 
                double speed = Math.sqrt(mDX * mDX + mDY * mDY);
 
                boolean onGoal = (mGoalX <= mX - mLanderWidth / 2 && mX
 
                        + mLanderWidth / 2 <= mGoalX + mGoalWidth);
 
                // "Hyperspace" win -- upside down, going fast,
 
                // puts you back at the top.
 
                if (onGoal && Math.abs(mHeading - 180) < mGoalAngle
 
                        && speed > PHYS_SPEED_HYPERSPACE) {
 
                    result = STATE_WIN;
 
                    mWinsInARow++;
 
                    doStart();
 
                    return;
 
                    // Oddball case: this case does a return, all other cases
 
                    // fall through to setMode() below.
 
                } else if (!onGoal) {
 
                    message = res.getText(R.string.message_off_pad);
 
                } else if (!(mHeading <= mGoalAngle || mHeading >= 360 - mGoalAngle)) {
 
                    message = res.getText(R.string.message_bad_angle);
 
                } else if (speed > mGoalSpeed) {
 
                    message = res.getText(R.string.message_too_fast);
 
                } else {
 
                    result = STATE_WIN;
 
                    mWinsInARow++;
 
                }
 
                setState(result, message);
 
            }
 
        }
 
    }
 
    /** Handle to the application context, used to e.g. fetch Drawables. */
 
    private Context mContext;
 
    /** Pointer to the text view to display "Paused.." etc. */
 
    private TextView mStatusText;
 
    /** The thread that actually draws the animation */
 
    private LunarThread thread;
 
    public LunarView(Context context, AttributeSet attrs) {
 
        super(context, attrs);
 
        // register our interest in hearing about changes to our surface
 
        SurfaceHolder holder = getHolder();
 
        holder.addCallback(this);
 
        // create thread only; it's started in surfaceCreated()
 
        thread = new LunarThread(holder, context, new Handler() {
 
            @Override
 
            public void handleMessage(Message m) {
 
                mStatusText.setVisibility(m.getData().getInt("viz"));
 
                mStatusText.setText(m.getData().getString("text"));
 
            }
 
        });
 
        setFocusable(true); // make sure we get key events
 
    }
 
    /**
 
     * Fetches the animation thread corresponding to this LunarView.
 
     * 
 
     * @return the animation thread
 
     */
 
    public LunarThread getThread() {
 
        return thread;
 
    }
 
    /**
 
     * Standard override to get key-press events.
 
     */
 
    @Override
 
    public boolean onKeyDown(int keyCode, KeyEvent msg) {
 
        return thread.doKeyDown(keyCode, msg);
 
    }
 
    /**
 
     * Standard override for key-up. We actually care about these, so we can
 
     * turn off the engine or stop rotating.
 
     */
 
    @Override
 
    public boolean onKeyUp(int keyCode, KeyEvent msg) {
 
        return thread.doKeyUp(keyCode, msg);
 
    }
 
    /**
 
     * Standard window-focus override. Notice focus lost so we can pause on
 
     * focus lost. e.g. user switches to take a call.
 
     */
 
    @Override
 
    public void onWindowFocusChanged(boolean hasWindowFocus) {
 
        if (!hasWindowFocus) thread.pause();
 
    }
 
    /**
 
     * Installs a pointer to the text view used for messages.
 
     */
 
    public void setTextView(TextView textView) {
 
        mStatusText = textView;
 
    }
 
    /* Callback invoked when the surface dimensions change. */
 
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
 
            int height) {
 
        thread.setSurfaceSize(width, height);
 
    }
 
    /*
 
     * Callback invoked when the Surface has been created and is ready to be
 
     * used.
 
     */
 
    public void surfaceCreated(SurfaceHolder holder) {
 
        // start the thread here so that we don't busy-wait in run()
 
        // waiting for the surface to be created
 
        thread.setRunning(true);
 
        thread.start();
 
    }
 
    /*
 
     * Callback invoked when the Surface has been destroyed and must no longer
 
     * be touched. WARNING: after this method returns, the Surface/Canvas must
 
     * never be touched again!
 
     */
 
    public void surfaceDestroyed(SurfaceHolder holder) {
 
        // we have to tell thread to shut down & wait for it to finish, or else
 
        // it might touch the Surface after we return and explode
 
        boolean retry = true;
 
        thread.setRunning(false);
 
        while (retry) {
 
            try {
 
                thread.join();
 
                retry = false;
 
            } catch (InterruptedException e) {
 
            }
 
        }
 
    }
 
}

Android游戏开发之旅九 VideoView类剖析

有关SurfaceView相关的内容今天Android123继续延用系统的示例类VideoView来让大家深入了解Android平台的图形绘制基础类的实现原理。大家可能会发现VideoView类的控制方面无法改变,我们可以通过重构VideoView类来实现更个性化的播放器。

public class VideoView extends SurfaceView implements MediaPlayerControl {
 
    private String TAG = "VideoView";
 
    // settable by the client
 
    private Uri         mUri;
 
    private int         mDuration;
 
    // all possible internal states
 
    private static final int STATE_ERROR              = -1;
 
    private static final int STATE_IDLE               = 0;
 
    private static final int STATE_PREPARING          = 1;
 
    private static final int STATE_PREPARED           = 2;
 
    private static final int STATE_PLAYING            = 3;
 
    private static final int STATE_PAUSED             = 4;
 
    private static final int STATE_PLAYBACK_COMPLETED = 5;
 
    // mCurrentState is a VideoView object's current state.
 
    // mTargetState is the state that a method caller intends to reach.
 
    // For instance, regardless the VideoView object's current state,
 
    // calling pause() intends to bring the object to a target state
 
    // of STATE_PAUSED.
 
    private int mCurrentState = STATE_IDLE;
 
    private int mTargetState  = STATE_IDLE;
 
    // All the stuff we need for playing and showing a video
 
    private SurfaceHolder mSurfaceHolder = null;
 
    private MediaPlayer mMediaPlayer = null;
 
    private int         mVideoWidth;
 
    private int         mVideoHeight;
 
    private int         mSurfaceWidth;
 
    private int         mSurfaceHeight;
 
    private MediaController mMediaController;
 
    private OnCompletionListener mOnCompletionListener;
 
    private MediaPlayer.OnPreparedListener mOnPreparedListener;
 
    private int         mCurrentBufferPercentage;
 
    private OnErrorListener mOnErrorListener;
 
    private int         mSeekWhenPrepared;  // recording the seek position while preparing
 
    private boolean     mCanPause;
 
    private boolean     mCanSeekBack;
 
    private boolean     mCanSeekForward;
 
    public VideoView(Context context) {
 
        super(context);
 
        initVideoView();
 
    }
 
 
 
    public VideoView(Context context, AttributeSet attrs) {
 
        this(context, attrs, 0);
 
        initVideoView();
 
    }
 
 
 
    public VideoView(Context context, AttributeSet attrs, int defStyle) {
 
        super(context, attrs, defStyle);
 
        initVideoView();
 
    }
 
    @Override
 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 
        //Log.i("@@@@", "onMeasure");
 
        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
 
        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
 
        if (mVideoWidth > 0 && mVideoHeight > 0) {
 
            if ( mVideoWidth * height  > width * mVideoHeight ) {
 
                //Log.i("@@@", "image too tall, correcting");
 
                height = width * mVideoHeight / mVideoWidth;
 
            } else if ( mVideoWidth * height  < width * mVideoHeight ) {
 
                //Log.i("@@@", "image too wide, correcting");
 
                width = height * mVideoWidth / mVideoHeight;
 
            } else {
 
                //Log.i("@@@", "aspect ratio is correct: " +
 
                        //width+"/"+height+"="+
 
                        //mVideoWidth+"/"+mVideoHeight);
 
            }
 
        }
 
        //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height);
 
        setMeasuredDimension(width, height);
 
    }
 
 
 
    public int resolveAdjustedSize(int desiredSize, int measureSpec) {
 
        int result = desiredSize;
 
        int specMode = MeasureSpec.getMode(measureSpec);
 
        int specSize =  MeasureSpec.getSize(measureSpec);
 
        switch (specMode) {
 
            case MeasureSpec.UNSPECIFIED:
 
                /* Parent says we can be as big as we want. Just don't be larger
 
                 * than max size imposed on ourselves.
 
                 */
 
                result = desiredSize;
 
                break;
 
            case MeasureSpec.AT_MOST:
 
                /* Parent says we can be as big as we want, up to specSize. 
 
                 * Don't be larger than specSize, and don't be larger than 
 
                 * the max size imposed on ourselves.
 
                 */
 
                result = Math.min(desiredSize, specSize);
 
                break;
 
 
 
            case MeasureSpec.EXACTLY:
 
                // No choice. Do what we are told.
 
                result = specSize;
 
                break;
 
        }
 
        return result;
 
}
 
 
 
    private void initVideoView() {
 
        mVideoWidth = 0;
 
        mVideoHeight = 0;
 
        getHolder().addCallback(mSHCallback);
 
        getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
 
        setFocusable(true);
 
        setFocusableInTouchMode(true);
 
        requestFocus();
 
        mCurrentState = STATE_IDLE;
 
        mTargetState  = STATE_IDLE;
 
    }
 
    public void setVideoPath(String path) {
 
        setVideoURI(Uri.parse(path));
 
    }
 
    public void setVideoURI(Uri uri) {
 
        mUri = uri;
 
        mSeekWhenPrepared = 0;
 
        openVideo();
 
        requestLayout();
 
        invalidate();
 
    }
 
 
 
    public void stopPlayback() {
 
        if (mMediaPlayer != null) {
 
            mMediaPlayer.stop();
 
            mMediaPlayer.release();
 
            mMediaPlayer = null;
 
            mCurrentState = STATE_IDLE;
 
            mTargetState  = STATE_IDLE;
 
        }
 
    }
 
    private void openVideo() {
 
        if (mUri == null || mSurfaceHolder == null) {
 
            // not ready for playback just yet, will try again later
 
            return;
 
        }
 
        // Tell the music playback service to pause 
 
        // TODO: these constants need to be published somewhere in the framework.
 
        Intent i = new Intent("com.android.music.musicservicecommand");
 
        i.putExtra("command", "pause");
 
        mContext.sendBroadcast(i);
 
        // we shouldn't clear the target state, because somebody might have
 
        // called start() previously
 
        release(false);
 
        try {
 
            mMediaPlayer = new MediaPlayer();
 
            mMediaPlayer.setOnPreparedListener(mPreparedListener);
 
            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
 
            mDuration = -1;
 
            mMediaPlayer.setOnCompletionListener(mCompletionListener);
 
            mMediaPlayer.setOnErrorListener(mErrorListener);
 
            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
 
            mCurrentBufferPercentage = 0;
 
            mMediaPlayer.setDataSource(mContext, mUri);
 
            mMediaPlayer.setDisplay(mSurfaceHolder);
 
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
 
            mMediaPlayer.setScreenOnWhilePlaying(true);
 
            mMediaPlayer.prepareAsync();
 
            // we don't set the target state here either, but preserve the
 
            // target state that was there before.
 
            mCurrentState = STATE_PREPARING;
 
            attachMediaController();
 
        } catch (IOException ex) {
 
            Log.w(TAG, "Unable to open content: " + mUri, ex);
 
            mCurrentState = STATE_ERROR;
 
            mTargetState = STATE_ERROR;
 
            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
 
            return;
 
        } catch (IllegalArgumentException ex) {
 
            Log.w(TAG, "Unable to open content: " + mUri, ex);
 
            mCurrentState = STATE_ERROR;
 
            mTargetState = STATE_ERROR;
 
            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
 
            return;
 
        }
 
    }
 
 
 
    public void setMediaController(MediaController controller) {
 
        if (mMediaController != null) {
 
            mMediaController.hide();
 
        }
 
        mMediaController = controller;
 
        attachMediaController();
 
    }
 
    private void attachMediaController() {
 
        if (mMediaPlayer != null && mMediaController != null) {
 
            mMediaController.setMediaPlayer(this);
 
            View anchorView = this.getParent() instanceof View ?
 
                    (View)this.getParent() : this;
 
            mMediaController.setAnchorView(anchorView);
 
            mMediaController.setEnabled(isInPlaybackState());
 
        }
 
    }
 
 
 
    MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
 
        new MediaPlayer.OnVideoSizeChangedListener() {
 
            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
 
                mVideoWidth = mp.getVideoWidth();
 
                mVideoHeight = mp.getVideoHeight();
 
                if (mVideoWidth != 0 && mVideoHeight != 0) {
 
                    getHolder().setFixedSize(mVideoWidth, mVideoHeight);
 
                }
 
            }
 
    };
 
 
 
    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
 
        public void onPrepared(MediaPlayer mp) {
 
            mCurrentState = STATE_PREPARED;
 
            // Get the capabilities of the player for this stream
 
            Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
 
                                      MediaPlayer.BYPASS_METADATA_FILTER);
 
            if (data != null) {
 
                mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
 
                        || data.getBoolean(Metadata.PAUSE_AVAILABLE);
 
                mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
 
                        || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
 
                mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
 
                        || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
 
            } else {
 
                mCanPause = mCanSeekForward = mCanSeekForward = true;
 
            }
 
            if (mOnPreparedListener != null) {
 
                mOnPreparedListener.onPrepared(mMediaPlayer);
 
            }
 
            if (mMediaController != null) {
 
                mMediaController.setEnabled(true);
 
            }
 
            mVideoWidth = mp.getVideoWidth();
 
            mVideoHeight = mp.getVideoHeight();
 
            int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
 
            if (seekToPosition != 0) {
 
                seekTo(seekToPosition);
 
            }
 
            if (mVideoWidth != 0 && mVideoHeight != 0) {
 
                //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
 
                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
 
                if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
 
                    // We didn't actually change the size (it was already at the size
 
                    // we need), so we won't get a "surface changed" callback, so
 
                    // start the video here instead of in the callback.
 
                    if (mTargetState == STATE_PLAYING) {
 
                        start();
 
                        if (mMediaController != null) {
 
                            mMediaController.show();
 
                        }
 
                    } else if (!isPlaying() &&
 
                               (seekToPosition != 0 || getCurrentPosition() > 0)) {
 
                       if (mMediaController != null) {
 
                           // Show the media controls when we're paused into a video and make 'em stick.
 
                           mMediaController.show(0);
 
                       }
 
                   }
 
                }
 
            } else {
 
                // We don't know the video size yet, but should start anyway.
 
                // The video size might be reported to us later.
 
                if (mTargetState == STATE_PLAYING) {
 
                    start();
 
                }
 
            }
 
        }
 
    };
 
    private MediaPlayer.OnCompletionListener mCompletionListener =
 
        new MediaPlayer.OnCompletionListener() {
 
        public void onCompletion(MediaPlayer mp) {
 
            mCurrentState = STATE_PLAYBACK_COMPLETED;
 
            mTargetState = STATE_PLAYBACK_COMPLETED;
 
            if (mMediaController != null) {
 
                mMediaController.hide();
 
            }
 
            if (mOnCompletionListener != null) {
 
                mOnCompletionListener.onCompletion(mMediaPlayer);
 
            }
 
        }
 
    };
 
    private MediaPlayer.OnErrorListener mErrorListener =
 
        new MediaPlayer.OnErrorListener() {
 
        public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
 
            Log.d(TAG, "Error: " + framework_err + "," + impl_err);
 
            mCurrentState = STATE_ERROR;
 
            mTargetState = STATE_ERROR;
 
            if (mMediaController != null) {
 
                mMediaController.hide();
 
            }
 
            /* If an error handler has been supplied, use it and finish. */
 
            if (mOnErrorListener != null) {
 
                if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
 
                    return true;
 
                }
 
            }
 
            /* Otherwise, pop up an error dialog so the user knows that
 
             * something bad has happened. Only try and pop up the dialog
 
             * if we're attached to a window. When we're going away and no
 
             * longer have a window, don't bother showing the user an error.
 
             */
 
            if (getWindowToken() != null) {
 
                Resources r = mContext.getResources();
 
                int messageId;
 
                if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
 
                    messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
 
                } else {
 
                    messageId = com.android.internal.R.string.VideoView_error_text_unknown;
 
                }
 
                new AlertDialog.Builder(mContext)
 
                        .setTitle(com.android.internal.R.string.VideoView_error_title)
 
                        .setMessage(messageId)
 
                        .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
 
                                new DialogInterface.OnClickListener() {
 
                                    public void onClick(DialogInterface dialog, int whichButton) {
 
                                        /* If we get here, there is no onError listener, so
 
                                         * at least inform them that the video is over.
 
                                         */
 
                                        if (mOnCompletionListener != null) {
 
                                            mOnCompletionListener.onCompletion(mMediaPlayer);
 
                                        }
 
                                    }
 
                                })
 
                        .setCancelable(false)
 
                        .show();
 
            }
 
            return true;
 
        }
 
    };
 
    private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
 
        new MediaPlayer.OnBufferingUpdateListener() {
 
        public void onBufferingUpdate(MediaPlayer mp, int percent) {
 
            mCurrentBufferPercentage = percent;
 
        }
 
    };
 
    /**
 
     * Register a callback to be invoked when the media file
 
     * is loaded and ready to go.
 
     *
 
     * @param l The callback that will be run
 
     */
 
    public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
 
    {
 
        mOnPreparedListener = l;
 
    }
 
    /**
 
     * Register a callback to be invoked when the end of a media file
 
     * has been reached during playback.
 
     *
 
     * @param l The callback that will be run
 
     */
 
    public void setOnCompletionListener(OnCompletionListener l)
 
    {
 
        mOnCompletionListener = l;
 
    }
 
    /**
 
     * Register a callback to be invoked when an error occurs
 
     * during playback or setup.  If no listener is specified,
 
     * or if the listener returned false, VideoView will inform
 
     * the user of any errors.
 
     *
 
     * @param l The callback that will be run
 
     */
 
    public void setOnErrorListener(OnErrorListener l)
 
    {
 
        mOnErrorListener = l;
 
    }
 
    SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
 
    {
 
        public void surfaceChanged(SurfaceHolder holder, int format,
 
                                    int w, int h)
 
        {
 
            mSurfaceWidth = w;
 
            mSurfaceHeight = h;
 
            boolean isValidState =  (mTargetState == STATE_PLAYING);
 
            boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
 
            if (mMediaPlayer != null && isValidState && hasValidSize) {
 
                if (mSeekWhenPrepared != 0) {
 
                    seekTo(mSeekWhenPrepared);
 
                }
 
                start();
 
                if (mMediaController != null) {
 
                    mMediaController.show();
 
                }
 
            }
 
        }
 
        public void surfaceCreated(SurfaceHolder holder)
 
        {
 
            mSurfaceHolder = holder;
 
            openVideo();
 
        }
 
        public void surfaceDestroyed(SurfaceHolder holder)
 
        {
 
            // after we return from this we can't use the surface any more
 
            mSurfaceHolder = null;
 
            if (mMediaController != null) mMediaController.hide();
 
            release(true);
 
        }
 
    };
 
    /*
 
     * release the media player in any state
 
     */
 
    private void release(boolean cleartargetstate) {
 
        if (mMediaPlayer != null) {
 
            mMediaPlayer.reset();
 
            mMediaPlayer.release();
 
            mMediaPlayer = null;
 
            mCurrentState = STATE_IDLE;
 
            if (cleartargetstate) {
 
                mTargetState  = STATE_IDLE;
 
            }
 
        }
 
    }
 
    @Override
 
    public boolean onTouchEvent(MotionEvent ev) {
 
        if (isInPlaybackState() && mMediaController != null) {
 
            toggleMediaControlsVisiblity();
 
        }
 
        return false;
 
    }
 
 
 
    @Override
 
    public boolean onTrackballEvent(MotionEvent ev) {
 
        if (isInPlaybackState() && mMediaController != null) {
 
            toggleMediaControlsVisiblity();
 
        }
 
        return false;
 
    }
 
 
 
    @Override
 
    public boolean onKeyDown(int keyCode, KeyEvent event)
 
    {
 
        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
 
                                     keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
 
                                     keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
 
                                     keyCode != KeyEvent.KEYCODE_MENU &&
 
                                     keyCode != KeyEvent.KEYCODE_CALL &&
 
                                     keyCode != KeyEvent.KEYCODE_ENDCALL;
 
        if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
 
            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
 
                    keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
 
                if (mMediaPlayer.isPlaying()) {
 
                    pause();
 
                    mMediaController.show();
 
                } else {
 
                    start();
 
                    mMediaController.hide();
 
                }
 
                return true;
 
            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 
 
                    && mMediaPlayer.isPlaying()) {
 
                pause();
 
                mMediaController.show();
 
            } else {
 
                toggleMediaControlsVisiblity();
 
            }
 
        }
 
        return super.onKeyDown(keyCode, event);
 
    }
 
    private void toggleMediaControlsVisiblity() {
 
        if (mMediaController.isShowing()) { 
 
            mMediaController.hide();
 
        } else {
 
            mMediaController.show();
 
        }
 
    }
 
 
 
    public void start() {
 
        if (isInPlaybackState()) {
 
            mMediaPlayer.start();
 
            mCurrentState = STATE_PLAYING;
 
        }
 
        mTargetState = STATE_PLAYING;
 
    }
 
 
 
    public void pause() {
 
        if (isInPlaybackState()) {
 
            if (mMediaPlayer.isPlaying()) {
 
                mMediaPlayer.pause();
 
                mCurrentState = STATE_PAUSED;
 
            }
 
        }
 
        mTargetState = STATE_PAUSED;
 
    }
 
 
 
    // cache duration as mDuration for faster access
 
    public int getDuration() {
 
        if (isInPlaybackState()) {
 
            if (mDuration > 0) {
 
                return mDuration;
 
            }
 
            mDuration = mMediaPlayer.getDuration();
 
            return mDuration;
 
        }
 
        mDuration = -1;
 
        return mDuration;
 
    }
 
 
 
    public int getCurrentPosition() {
 
        if (isInPlaybackState()) {
 
            return mMediaPlayer.getCurrentPosition();
 
        }
 
        return 0;
 
    }
 
 
 
    public void seekTo(int msec) {
 
        if (isInPlaybackState()) {
 
            mMediaPlayer.seekTo(msec);
 
            mSeekWhenPrepared = 0;
 
        } else {
 
            mSeekWhenPrepared = msec;
 
        }
 
    }    
 
 
 
    public boolean isPlaying() {
 
        return isInPlaybackState() && mMediaPlayer.isPlaying();
 
    }
 
 
 
    public int getBufferPercentage() {
 
        if (mMediaPlayer != null) {
 
            return mCurrentBufferPercentage;
 
        }
 
        return 0;
 
    }
 
    private boolean isInPlaybackState() {
 
        return (mMediaPlayer != null &&
 
                mCurrentState != STATE_ERROR &&
 
                mCurrentState != STATE_IDLE &&
 
                mCurrentState != STATE_PREPARING);
 
    }
 
    public boolean canPause() {
 
        return mCanPause;
 
    }
 
    public boolean canSeekBackward() {
 
        return mCanSeekBack;
 
    }
 
    public boolean canSeekForward() {
 
        return mCanSeekForward;
 
    }
 
}

Android游戏开发之旅十 位图旋转

今天有关Android游戏开发的基础,我们说下Bitmap相关的实用操作,这里我们就说下位图旋转。在Android中图形的旋转和变化提供了方便的矩阵Maxtrix类,Maxtrix类的setRotate方法接受图形的变换角度和缩放,最终Bitmap类的createBitmap方法中其中的重载函数,可以接受Maxtrix对象,方法原型如下

 public static Bitmap createBitmap (Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter) 
参数的具体意思
source 源bitmap对象
x  源坐标x位置
y 源坐标y位置
width  宽度
height  高度 
m  接受的maxtrix对象,如果没有可以设置为null
filter 该参数仅对maxtrix包含了超过一个翻转才有效。

下面Android123给大家一个比较经典的例子,rotate方法是静态方法可以直接调用,参数为源Bitmap对象,参数二为旋转的角度,从0~360,返回值为新的Bitmap对象。其中具体的宽高可以调整。

public static Bitmap rotate(Bitmap b, int degrees) {
 
        if (degrees != 0 && b != null) {
 
            Matrix m = new Matrix();
 
            m.setRotate(degrees,
 
                    (float) b.getWidth() / 2, (float) b.getHeight() / 2);
 
            try {
 
                Bitmap b2 = Bitmap.createBitmap(
 
                        b, 0, 0, b.getWidth(), b.getHeight(), m, true);
 
                if (b != b2) {
 
                    b.recycle();  //Android开发网再次提示Bitmap操作完应该显示的释放
 
                    b = b2;
 
                }
 
            } catch (OutOfMemoryError ex) {
 
                // Android123建议大家如何出现了内存不足异常,最好return 原始的bitmap对象。.
 
            }
 
        }
 
        return b;
 
    }

Android游戏开发之旅11 View中手势识别

有关Android平台的游戏开发中我们需要涉及到控制,在开始的Android游戏开发之旅中我们提到了按键和轨迹球的控制方式,从今天开始Android123开始给出大家游戏中其他的一些控制方式,比如今天的手势操作和未来重力感应。

很多网友发现Android中手势识别提供了两个类,由于Android 1.6以下的版本比如cupcake中无法使用android.view.GestureDetector,而android.gesture.Gesture是Android 1.6才开始支持的,我们考虑到仍然有很多Android 1.5固件的网友,就来看下兼容性更强的android.view.GestureDetector。在android.view.GestureDetector类中有很多种重载版本,下面我们仅提到能够自定义在View中的两种方法,分别为GestureDetector(Context context, GestureDetector.OnGestureListener listener) 和 GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler) 和。我们可以看到第一个参数为Context,所以我们想附着到某View时,最简单的方法就是直接从超类派生传递Context,实现GestureDetector里中提供一些接口。

下面我们就以实现手势识别的onFling动作,在CwjView中我们从View类继承,当然大家可以从TextView等更高层的界面中实现触控。

class CwjView extends View {
    private GestureDetector mGD;
    public CwjView(Context context, AttributeSet attrs) {
        super(context, attrs);
 
        mGD = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                    int dx = (int) (e2.getX() - e1.getX()); //计算滑动的距离
                    if (Math.abs(dx) > MAJOR_MOVE && Math.abs(velocityX) > Math.abs(velocityY)) { //降噪处理,必须有较大的动作才识别
                        if (velocityX > 0) {
                           //向右边
                        } else {
                            //向左边
                        }
                        return true;
                    } else {
                        return false;  //当然可以处理velocityY处理向上和向下的动作
                    }
                }
            });
    }

在上面Android123提示大家仅仅探测了Fling动作仅仅实现了onFling方法,这里相关的还有以下几种方法来实现具体的可以参考我们以前的文章有详细的解释:

boolean  onDoubleTap(MotionEvent e) 
boolean  onDoubleTapEvent(MotionEvent e) 
boolean  onDown(MotionEvent e) 
void  onLongPress(MotionEvent e) 
boolean  onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 
void  onShowPress(MotionEvent e) 
boolean  onSingleTapConfirmed(MotionEvent e) 
boolean  onSingleTapUp(MotionEvent e) 


接下来是重点,让我们的View接受触控,需要使用下面两个方法让GestureDetector类去处理onTouchEvent和onInterceptTouchEvent方法。

    @Override
    public boolean onTouchEvent(MotionEvent event) {  
        mGD.onTouchEvent(event);
        return true;
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mGD.onTouchEvent(event);
    }
}

有关重力感应的方向识别, 我们将告诉大家如何通过重力感应来控制我们的Android游戏,目前对于大多数Android设备来说仅提供了重力感应器和加速感应器,只有较新或高端的Android设备还提供了陀螺仪,可以帮助我们测试角速度,来处理一些复杂的应用。详细的可以参考我们的 Android游戏开发之旅12 重力感应篇

Android游戏开发之旅12 Sensor重力感应

从Android手机开始,主流的智能机纷纷加入了感应器Sensor硬件,常见的有光线感应器、重力感应器、加速感应器,而更高级的有磁极方向、陀螺仪、距离感应器、温度感应器等等。对于Android游戏开发,我们主要用到重力、加速、磁力和陀螺仪四种,当然部分游戏可能需要GPS或Cellid定位来修正一些位移信息。从系统中提高的感应器主要在android.hardware中,我们可以看到系统提供了android.hardware.SensorEventListener、Sensor和SensorManager这三个类,我们会发现除了可以获取感应器的信息,和感应器的原始数据外,并没有提供相关的逻辑处理。Android123将会分3篇来详细的介绍不同感应器的作用和逻辑处理,比如自由落体,晃动,磁极,当前的旋转速度。

未来Android123将完成主要是一个基于OpenGL 3D的雷电游戏,最终加入联网对战效果可以团队打怪实现手机3D网游充分发挥Android手机的娱乐能力。对于大多数新款Android手机可能没有配备轨迹球或导航键的方向控制,所以重力感应器是这类实时性较强游戏的首选控制方式。主要有以下几点问题对于Sensor

1. 降噪处理,如果做过LBS软件的大家可能明白偏移修正,在GPS无法正常获取数据较间断时地图不能乱飘,这里Sensor也不例外,除了使用采样数据平均值获取外,可以间隔采样的方法来处理。细节的算法我们将在下节给出示例代码。

2. 感应器的敏感度,在Android中提供了四种延迟级别分别为

SENSOR_DELAY_FASTEST 最低延迟,一般不是特别敏感的处理不推荐使用,该种模式可能造成手机电力大量消耗,由于传递的为原始数据,算法不处理好将会影响游戏逻辑和UI的性能,所以Android开发网不推荐大家使用。

SENSOR_DELAY_GAME 游戏延迟,一般绝大多数的实时性较高的游戏都使用该级别

int SENSOR_DELAY_NORMAL 标准延迟,对于一般的益智类或EASY级别的游戏可以使用,但过低的采样率可能对一些赛车类游戏有跳帧现象。

int SENSOR_DELAY_UI 用户界面延迟,一般对于屏幕方向自动旋转使用,相对节省电能和逻辑处理,一般游戏开发中我们不使用。


Android游戏开发之旅(十三)Sensor感应示例

有关Android游戏开发中的Sensor感应示例今天我们将一起来讨论,对于目前最新的Android 2.2平台而言仍然没有具体的感应判断逻辑,下面我们一起定义下常用的感应动作事件。首先Android123提醒大家由于是三轴的立体空间感应所以相对于轨迹球、导航键的上下左右外,还提供了前后的感应,所以我们定义最基本的六种空间方向。

public static final int CWJ_UP = 0;

public static final int CWJ_DOWN = 1;

public static final int CWJ_LEFT = 2;

public static final int CWJ_RIGHT = 4;

public static final int CWJ_FORWARD = 8; //向前

public static final int CWJ_BACKWARD = 16; //向后

复制代码下面我们做精确的角度旋转修正值定义,我们用到yaw、pitch和roll,相信学过3D开发的网友不会对这些陌生的,我们就把他们对应为绕y、x、z 轴的角度好了,如果你们没有学过3D相关的知识这里Android开发网推荐大家可以通过Cube例子自定义Render来观察这三个值对应立方体的旋转角度。

Yaw在(0,0,0)中, 以xOz的坐标平面中围绕y轴旋转,如果是负角则我们定义为CWJ_YAW_LEFT 即往左边倾斜,同理我们定义如下:

public static final int CWJ_YAW_LEFT = 0;

public static final int CWJ_YAW_RIGHT = 1;

public static final int CWJ_PITCH_UP = 2;

public static final int CWJ_PITCH_DOWN = 4;

public static final int CWJ_ROLL_LEFT = 8;

public static final int CWJ_ROLL_RIGHT = 16;

复制代码我们通过加速感应器可以获得SensorEvent的四个值,今天Android123给大家一个简单示例,不考虑其他因素,在public int accuracy 、public Sensor sensor 、public long timestamp 和 public final float[] values 中,我们获取values的浮点数组来判断方向。

     int nAndroid123=CWJ_UP //向上
 
     float ax = values[0];
 
    float ay = values[1];
 
    float az = values[2];
 
    float absx = Math.abs(ax);
 
    float absy = Math.abs(ay);
 
    float absz = Math.abs(az);
 
    if (absx > absy && absx > absz) {
 
      if (ax > 0) {
 
        nAndroid123 = CWJ_RIGHT;
 
      } else {
 
        nAndroid123 = CWJ_LEFT;
 
      }
 
      } else if (absy > absx && absy > absz) {
 
 
 
      if (ay > 0) {
 
      nAndroid123= CWJ_FORWARD;
 
      } else {
 
      nAndroid123= CWJ_BACKWARD;
 
      }
 
      } else if (absz > absx && absz > absy) {
 
 
 
      if (az > 0) {
 
        nAndroid123 = CWJ_UP;
 
      } else {
 
        nAndroid123 = CWJ_DOWN;
 
      }
 
      } else {
 
      nAndroid123 = CWJ_UNKNOWN;
 
      }

复制代码有关偏向角度问题,我们将在下一次详细讲述,对于一般的2D游戏,我们可以参考本文来实现重力控制,所以总体来说Android游戏开发比较简单易懂,Android平台使用的Java语言还是很适合做游戏的。在逻辑表达上更清晰。


Android游戏开发之旅(十四)游戏开发实战一

从今天开始Android123将开始带领大家进入Android游戏开发实战篇,本次我们首个游戏为2D的基于SurfaceView的类似横版卷轴游戏。第一天我们说下需要做哪些准备:

一、游戏地图编辑器,在J2ME时代我们可能都是用GIF分割多帧或BMP上放置多个图片通过减少文件头来压缩体积,但是在Android平台上开发游戏我们不需要那么节省,不过资源的释放仍然很重要,否则会出现OutOfMemoryError这样的悲剧发生。一般简单的2D平面游戏地图都是使用二维数组来标记的。我们可以想象矩阵中的每个元素对应每个图片资源。详细的存储方法我们将在下次具体讲到。

二、控制方式,由于横版过关类游戏不适合重力感应操作,我们这里选择屏幕下方加设一个区域,放置上、下、左、右按键,同时右侧给出常用的攻击、跳跃按钮,而游戏的暂停可以通过触控实现继续或暂停。

三、音效处理,常规的一般在攻击比如出拳、发射子弹的过程中有音效,或对手自己中弹(当然对于Android图形开发来说就是碰撞检测)时发出音效,跳跃、过关均会需要一些声音素材文件,一般的游戏还需要背景音乐配合烘托游戏气氛。

四、游戏逻辑,这是主要的地方,我们将通过实例代码让大家了解游戏开发中是如何的卷轴、人物的跳跃、攻击有效判断即碰撞检测,电脑智能等算法问题。

五、细节处理,比如计分,等级,游戏计时,关卡档案的存档,读取以及开场设计,关卡过渡的过场动画处理。

这里Android开发网提示大家,目前很多J2ME的游戏可以轻松的移植到Android平台,主要的细节只要了解Google Android平台的图形相关问题即可,主要是Bitmap、Drawable和View线程处理问题。


有关Android平台上游戏开发中我们需要处理一些特别的按键事件,对于突发的事情我们需要特别的考虑,比如突然来电话了和游戏中按下一些特殊的键,比如拍照键

@Override
 
    public boolean dispatchKeyEvent(KeyEvent event)
 
    {
 
          switch (event.getKeyCode())
 
          {
 
            case KeyEvent.KEYCODE_VOLUME_UP:  //音量键+
 
            case KeyEvent.KEYCODE_VOLUME_DOWN:  //音量键-
 
            case KeyEvent.KEYCODE_CAMERA:  //拍照键
 
            case KeyEvent.KEYCODE_FOCUS:  //拍照键半按的对焦状态
 
          //  event.getAction() == KeyEvent.ACTION_UP  //Android123提示如果按键按下后弹起时触发
 
 
 
            }
 
                return true; //这些标记为处理过,则不在往内部传递
 
        default:
 
                break;
 
        }
 
        return super.dispatchKeyEvent(event);
 
    }


Android游戏开发之旅(十五)按键中断处理

有关Android平台上游戏开发中我们需要处理一些特别的按键事件,对于突发的事情我们需要特别的考虑,比如突然来电话了和游戏中按下一些特殊的键,比如拍照键 @Override

    public boolean dispatchKeyEvent(KeyEvent event)
 
    {
 
          switch (event.getKeyCode())
 
          {
 
            case KeyEvent.KEYCODE_VOLUME_UP:  //音量键+
 
            case KeyEvent.KEYCODE_VOLUME_DOWN:  //音量键-
 
            case KeyEvent.KEYCODE_CAMERA:  //拍照键
 
            case KeyEvent.KEYCODE_FOCUS:  //拍照键半按的对焦状态
 
          //  event.getAction() == KeyEvent.ACTION_UP  //Android123提示如果按键按下后弹起时触发
 
 
 
            }
 
                return true; //这些标记为处理过,则不在往内部传递
 
        default:
 
                break;
 
        }
 
        return super.dispatchKeyEvent(event);
 
    }

复制代码对于游戏突然来电话我们一般采取通过PhoneStateListener类提供的public void onCallStateChanged (int state, String incomingNumber) 回调方法可以获取电话的状态,比如常规空闲时CALL_STATE_IDLE、来电时CALL_STATE_RINGING和 CALL_STATE_OFFHOOK 摘机通话中,有关处理的细节网友可以查看Android Git项目中的Music,在Android开源项目中系统自带的音乐播放器可以很好的处理,比如在通话结束后恢复音乐播放,而我们游戏需要做的就是记住当前的游戏状态尽量数据持久化处理,不能因为长时间的通话,游戏的Activity被清理了,这里我们一般通过onSaveInstanceState来保存当前窗口的一些记录,通过Intent标记来让系统管理好我们游戏的生命周期。


Android游戏开发之旅(十六)异步音乐播放

在Android游戏开发中我们必须考虑背景音乐播放问题,在Android平台中提供了MediaPlayer类可以播放声音,但是游戏除了播放音乐外还需要考虑画面的流畅性,以及多种音效同时播放,所以必须用到Android多线程机制和异步音效播放。Android SDK从1.0开始就提供了AsyncPlayer类,这里我们为了根据我们自己的需要可以派生或修改出更灵活的播放类。

import android.content.Context;
 
import android.net.Uri;
 
import android.os.PowerManager;
 
import android.os.SystemClock;
 
import android.util.Log;
 
 
 
import java.io.IOException;
 
import java.lang.IllegalStateException;
 
import java.util.LinkedList;
 
 
 
 
 
public class AsyncPlayer {
 
    private static final int PLAY = 1;
 
    private static final int STOP = 2;
 
    private static final boolean mDebug = false;
 
 
 
    private static final class Command {
 
        int code;
 
        Context context;
 
        Uri uri;
 
        boolean looping;
 
        int stream;
 
        long requestTime;
 
 
 
        public String toString() {
 
            return "{ code=" + code + " looping=" + looping + " stream=" + stream
 
                    + " uri=" + uri + " }";
 
        }
 
    }
 
 
 
    private LinkedList<Command> mCmdQueue = new LinkedList();  //用一个链表保存播放参数队列
 
 
 
    private void startSound(Command cmd) {
 
 
 
        try {
 
            MediaPlayer player = new MediaPlayer();
 
            player.setAudioStreamType(cmd.stream);
 
            player.setDataSource(cmd.context, cmd.uri);  //设置媒体源,这里Android123提示大家本类的public void play (Context context, Uri uri, boolean looping, int stream) 类第二个参数Uri为媒体位置。
 
            player.setLooping(cmd.looping);
 
            player.prepare();
 
            player.start();
 
            if (mPlayer != null) {
 
                mPlayer.release();
 
            }
 
            mPlayer = player;
 
          }
 
        catch (IOException e) {
 
            Log.w(mTag, "error loading sound for " + cmd.uri, e);
 
        } catch (IllegalStateException e) {
 
            Log.w(mTag, "IllegalStateException (content provider died?) " + cmd.uri, e);
 
        }
 
    }
 
 
 
    private final class Thread extends java.lang.Thread {  //通过多线程方式不阻塞调用者
 
        Thread() {
 
            super("AsyncPlayer-" + mTag);
 
        }
 
 
 
        public void run() {
 
            while (true) {
 
                Command cmd = null;
 
 
 
                synchronized (mCmdQueue) {  //同步方式执行
 
 
 
                        cmd = mCmdQueue.removeFirst();
 
                }
 
 
 
                switch (cmd.code) {
 
                case PLAY:
 
                                startSound(cmd);
 
                    break;
 
                case STOP:
 
 
 
                    if (mPlayer != null) {
 
                                        mPlayer.stop();
 
                        mPlayer.release();
 
                        mPlayer = null;
 
                    } else {
 
                        Log.w(mTag, "STOP command without a player");
 
                    }
 
                    break;
 
                }
 
 
 
                synchronized (mCmdQueue) {
 
                    if (mCmdQueue.size() == 0) {
 
 
 
                        mThread = null;
 
                        releaseWakeLock();
 
                        return;
 
                    }
 
                }
 
            }
 
        }
 
    }
 
 
 
    private String mTag;
 
    private Thread mThread;
 
    private MediaPlayer mPlayer;
 
    private PowerManager.WakeLock mWakeLock;
 
 
 
 
 
    private int mState = STOP;
 
 
 
    public AsyncPlayer(String tag) {
 
        if (tag != null) {
 
            mTag = tag;
 
        } else {
 
            mTag = "AsyncPlayer";
 
        }
 
    }
 
 
 
 
 
    public void play(Context context, Uri uri, boolean looping, int stream) {
 
        Command cmd = new Command();
 
        cmd.requestTime = SystemClock.uptimeMillis(); //这里为了测试性能,传递了开始执行前的系统tickcount计时器值
 
        cmd.code = PLAY;
 
        cmd.context = context;
 
        cmd.uri = uri;
 
        cmd.looping = looping;
 
        cmd.stream = stream;
 
        synchronized (mCmdQueue) {
 
            enqueueLocked(cmd);
 
            mState = PLAY;
 
        }
 
    }
 
 
 
 
 
    public void stop() {
 
        synchronized (mCmdQueue) {
 
                if (mState != STOP) {
 
                Command cmd = new Command();
 
                cmd.requestTime = SystemClock.uptimeMillis();
 
                cmd.code = STOP;
 
                enqueueLocked(cmd);
 
                mState = STOP;
 
            }
 
        }
 
    }
 
 
 
    private void enqueueLocked(Command cmd) {
 
        mCmdQueue.add(cmd);
 
        if (mThread == null) {
 
            acquireWakeLock();
 
            mThread = new Thread();
 
            mThread.start();
 
        }
 
    }
 
 
 
  一般对于Android游戏而言下面的代码不用考虑,一般用户都在交互操作,不会出现屏幕锁问题
 
 
 
    public void setUsesWakeLock(Context context) {  //电源管理wakelock处理
 
        if (mWakeLock != null || mThread != null) {
 
                      throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock
 
                    + " mThread=" + mThread);
 
        }
 
        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
 
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
 
    }
 
 
 
    private void acquireWakeLock() {  //加锁
 
        if (mWakeLock != null) {
 
            mWakeLock.acquire();
 
        }
 
    }
 
 
 
    private void releaseWakeLock() { //解锁
 
        if (mWakeLock != null) {
 
            mWakeLock.release();
 
        }
 
    }
 
}


參考


http://dev.10086.cn/cmdn/bbs/thread-19005-1-1.html?from=home