Android SurfaceView使用详解(很好的实战例子)

一、 surfaceview 主要作用

(1)视频输出的屏幕。(这个用得比较多)

(2)绘制图像

 

二、注意事项
surfaceCreated有特定的生命周期,注意在callback中进行操作。(具体请看例子)

三、清屏操作(绘制图像)

public void clearDraw(SurfaceHolder holder,int color) {
    Log.w("tan","clearDraw");
    Canvas canvas = null;
    try {
        canvas = holder.lockCanvas(null);
        canvas.drawColor(color);
    }catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();
    }finally {
        if(canvas != null) {
            holder.unlockCanvasAndPost(canvas);
        }
    }
}

 

------------------------------------------------- 正文  ---------------------------------- 

一、视频输出的屏幕

(1)设置surfaceview的参数

(2)在SurfaceCallback方法进行视频的播放控制。播放控制时,一般会进行播放器与surfaceview的绑定操作(如下)。注意这个getHolder。无论是surfaceview 还是TextureView都有这个holder。是视频输出的 关键。

mediaPlayer.setDisplay(surfaceView.getHolder());

代码:

public class MainActivity extends Activity {
    private EditText nameText;
    private String path;
    private MediaPlayer mediaPlayer;
    private SurfaceView surfaceView;
    private boolean pause;
    private int position;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mediaPlayer = new MediaPlayer();
        nameText = (EditText) this.findViewById(R.id.filename);
        surfaceView = (SurfaceView) this.findViewById(R.id.surfaceView);
        //把输送给surfaceView的视频画面,直接显示到屏幕上,不要维持它自身的缓冲区
        surfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        surfaceView.getHolder().setFixedSize(176, 144);
        surfaceView.getHolder().setKeepScreenOn(true);
        surfaceView.getHolder().addCallback(new SurfaceCallback());
    }

    private final class SurfaceCallback implements Callback{
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        }
        public void surfaceCreated(SurfaceHolder holder) {
            if(position>0 && path!=null){
                play(position);
                position = 0;
            }
        }
        public void surfaceDestroyed(SurfaceHolder holder) {
            if(mediaPlayer.isPlaying()){
                position = mediaPlayer.getCurrentPosition();
                mediaPlayer.stop();
            }
        }
    }

    @Override
    protected void onDestroy() {
        mediaPlayer.release();
        mediaPlayer = null;
        super.onDestroy();
    }

    public void mediaplay(View v){
        switch (v.getId()) {
            case R.id.playbutton:
                String filename = nameText.getText().toString();
                if(filename.startsWith("http")){
                    path = filename;
                    play(0);
                }else{
                    File file = new File(Environment.getExternalStorageDirectory(), filename);
                    if(file.exists()){
                        path = file.getAbsolutePath();
                        play(0);
                    }else{
                        path = null;
                        Toast.makeText(this, R.string.filenoexsit, 1).show();
                    }
                }

                break;

            case R.id.pausebutton:
                if(mediaPlayer.isPlaying()){
                    mediaPlayer.pause();
                    pause = true;
                }else{
                    if(pause){
                        mediaPlayer.start();
                        pause = false;
                    }
                }
                break;

            case R.id.resetbutton:
                if(mediaPlayer.isPlaying()){
                    mediaPlayer.seekTo(0);
                }else{
                    if(path!=null){
                        play(0);
                    }
                }
                break;
            case R.id.stopbutton:
                if(mediaPlayer.isPlaying()){
                    mediaPlayer.stop();
                }
                break;
        }
    }

    private void play(int position) {
        try {
            mediaPlayer.reset();
            mediaPlayer.setDataSource(path);
            mediaPlayer.setDisplay(surfaceView.getHolder());
            mediaPlayer.prepare();//缓冲
            mediaPlayer.setOnPreparedListener(new PrepareListener(position));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private final class PrepareListener implements OnPreparedListener{
        private int position;

        public PrepareListener(int position) {
            this.position = position;
        }

        public void onPrepared(MediaPlayer mp) {
            mediaPlayer.start();//播放视频
            if(position>0) mediaPlayer.seekTo(position);
        }
    }
}

 

-----------------------------------------------------------------------------------

二、绘制图像

1. SurfaceView的定义
前面已经介绍过View了,下面来简单介绍一下SurfaceView,参考SDK文档和网络资料:SurfaceView是View的子类,它内嵌了一个专门用于绘制的Surface,你可以控制这个Surface的格式和尺寸,Surfaceview控制这个Surface的绘制位置。surface是纵深排序(Z-ordered)的,说明它总在自己所在窗口的后面。SurfaceView提供了一个可见区域,只有在这个可见区域内的surface内容才可见。surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面有透明控件,那么每次surface变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
SurfaceView默认使用双缓冲技术的,它支持在子线程中绘制图像,这样就不会阻塞主线程了,所以它更适合于游戏的开发。

2. SurfaceView的使用
首先继承SurfaceView,并实现SurfaceHolder.Callback接口,实现它的三个方法:surfaceCreated,surfaceChanged,surfaceDestroyed。
surfaceCreated(SurfaceHolder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。
surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。
surfaceDestroyed(SurfaceHolder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。
还需要获得SurfaceHolder,并添加回调函数,这样这三个方法才会执行。

3. SurfaceView实战
下面通过一个小demo来学习SurfaceView在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:

 

首先创建核心类GameView.java,源码如下:

public class GameView extends SurfaceView implements
        SurfaceHolder.Callback {
    //屏幕宽高
    public static int SCREEN_WIDTH;
    public static int SCREEN_HEIGHT;

    private Context mContext;

    private SurfaceHolder mHolder;

    //最大帧数 (1000 / 30)
    private static final int DRAW_INTERVAL = 30;

    private DrawThread mDrawThread;

    private FrameAnimation[] spriteAnimations;

    private Sprite mSprite;

    private int spriteWidth = 0;

    private int spriteHeight = 0;

    private float spriteSpeed = (float) ((500 * SCREEN_WIDTH / 480) * 0.001);

    private int row = 4;

    private int col = 4;


    public GameSurfaceView(Context context) {

        super(context);

        this.mContext = context;

        mHolder = this.getHolder();

        mHolder.addCallback(this);

        initResources();


        mSprite = new Sprite(spriteAnimations, 0, 0, spriteWidth, spriteHeight, spriteSpeed);

    }


    private void initResources() {

        Bitmap[][] spriteImgs = generateBitmapArray(mContext, R.drawable.sprite, row, col);

        spriteAnimations = new FrameAnimation[row];

        for (int i = 0; i < row; i++) {

            Bitmap[] spriteImg = spriteImgs[i];

            FrameAnimation spriteAnimation = new FrameAnimation(spriteImg, new int[]{150, 150, 150, 150}, true);

            spriteAnimations[i] = spriteAnimation;

        }

    }


    public Bitmap decodeBitmapFromRes(Context context, int resourseId) {

        BitmapFactory.Options opt = new BitmapFactory.Options();

        opt.inPreferredConfig = Bitmap.Config.RGB_565;

        opt.inPurgeable = true;

        opt.inInputShareable = true;


        InputStream is = context.getResources().openRawResource(resourseId);

        return BitmapFactory.decodeStream(is, null, opt);

    }


    public Bitmap createBitmap(Context context, Bitmap source, int row,

                               int col, int rowTotal, int colTotal) {

        Bitmap bitmap = Bitmap.createBitmap(source,

                (col - 1) * source.getWidth() / colTotal,

                (row - 1) * source.getHeight() / rowTotal, source.getWidth()

                        / colTotal, source.getHeight() / rowTotal);

        return bitmap;

    }


    public Bitmap[][] generateBitmapArray(Context context, int resourseId,

                                          int row, int col) {

        Bitmap bitmaps[][] = new Bitmap[row][col];

        Bitmap source = decodeBitmapFromRes(context, resourseId);

        this.spriteWidth = source.getWidth() / col;

        this.spriteHeight = source.getHeight() / row;

        for (int i = 1; i <= row; i++) {
            for (int j = 1; j <= col; j++) {
                bitmaps[i - 1][j - 1] = createBitmap(context, source, i, j,
                        row, col);
            }
        }

        if (source != null && !source.isRecycled()) {

            source.recycle();

            source = null;

        }

        return bitmaps;

    }


    public void surfaceChanged(SurfaceHolder holder, int format, int width,

                               int height) {

    }


    public void surfaceCreated(SurfaceHolder holder) {

        if (null == mDrawThread) {

            mDrawThread = new DrawThread();

            mDrawThread.start();

        }

    }


    public void surfaceDestroyed(SurfaceHolder holder) {

        if (null != mDrawThread) {

            mDrawThread.stopThread();

        }

    }


    private class DrawThread extends Thread {

        public boolean isRunning = false;


        public DrawThread() {

            isRunning = true;

        }


        public void stopThread() {

            isRunning = false;

            boolean workIsNotFinish = true;

            while (workIsNotFinish) {

                try {

                    this.join();// 保证run方法执行完毕

                } catch (InterruptedException e) {

                    // TODO Auto-generated catch block

                    e.printStackTrace();

                }

                workIsNotFinish = false;

            }

        }


        public void run() {

            long deltaTime = 0;

            long tickTime = 0;

            tickTime = System.currentTimeMillis();

            while (isRunning) {

                Canvas canvas = null;

                try {

                    synchronized (mHolder) {

                        canvas = mHolder.lockCanvas();

                        //设置方向

                        mSprite.setDirection();

                        //更新精灵位置

                        mSprite.updatePosition(deltaTime);

                        drawSprite(canvas);

                    }

                } catch (Exception e) {

                    e.printStackTrace();

                } finally {

                    if (null != mHolder) {

                        mHolder.unlockCanvasAndPost(canvas);

                    }

                }


                deltaTime = System.currentTimeMillis() - tickTime;

                if (deltaTime < DRAW_INTERVAL) {

                    try {

                        Thread.sleep(DRAW_INTERVAL - deltaTime);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

                tickTime = System.currentTimeMillis();

            }


        }

    }


    private void drawSprite(Canvas canvas) {

        //清屏操作

        canvas.drawColor(Color.BLACK);

        mSprite.draw(canvas);

    }


}

 

 

GameView.java中包含了一个绘图线程DrawThread,在线程的run方法中锁定Canvas、绘制精灵、更新精灵位置、释放Canvas等操作。因为精灵素材是一张大图,所以这里进行了裁剪生成一个二维数组。使用这个二维数组初始化了精灵四个方向的动画,下面看Sprite.java的源码。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

public class Sprite {

 

    public static final int DOWN = 0;

    public static final int LEFT = 1;

    public static final int RIGHT = 2;

    public static final int UP = 3;

 

    public float x;

    public float y;

    public int width;

    public int height;

    //精灵行走速度

    public double speed;

    //精灵当前行走方向

    public int direction;

    //精灵四个方向的动画

    public FrameAnimation[] frameAnimations;

 

    public Sprite(FrameAnimation[] frameAnimations, int positionX,

            int positionY, int width, int height, float speed) {

        this.frameAnimations = frameAnimations;

        this.x = positionX;

        this.y = positionY;

        this.width = width;

        this.height = height;

        this.speed = speed;

    }

 

    public void updatePosition(long deltaTime) {

        switch (direction) {

        case LEFT:

            //让物体的移动速度不受机器性能的影响,每帧精灵需要移动的距离为:移动速度*时间间隔

            this.x = this.x - (float) (this.speed * deltaTime);

            break;

        case DOWN:

            this.y = this.y + (float) (this.speed * deltaTime);

            break;

        case RIGHT:

            this.x = this.x + (float) (this.speed * deltaTime);

            break;

        case UP:

            this.y = this.y - (float) (this.speed * deltaTime);

            break;

        }

    }

 

    /**

     * 根据精灵的当前位置判断是否改变行走方向

     */

    public void setDirection() {

        if (this.x <= 0

                && (this.y + this.height) < GameSurfaceView.SCREEN_HEIGHT) {

            if (this.x < 0)

                this.x = 0;

            this.direction = Sprite.DOWN;

        else if ((this.y + this.height) >= GameSurfaceView.SCREEN_HEIGHT

                && (this.x + this.width) < GameSurfaceView.SCREEN_WIDTH) {

            if ((this.y + this.height) > GameSurfaceView.SCREEN_HEIGHT)

                this.y = GameSurfaceView.SCREEN_HEIGHT - this.height;

            this.direction = Sprite.RIGHT;

        else if ((this.x + this.width) >= GameSurfaceView.SCREEN_WIDTH

                && this.y > 0) {

            if ((this.x + this.width) > GameSurfaceView.SCREEN_WIDTH)

                this.x = GameSurfaceView.SCREEN_WIDTH - this.width;

            this.direction = Sprite.UP;

        else {

            if (this.y < 0)

                this.y = 0;

            this.direction = Sprite.LEFT;

        }

 

    }

 

    public void draw(Canvas canvas) {

        FrameAnimation frameAnimation = frameAnimations[this.direction];

        Bitmap bitmap = frameAnimation.nextFrame();

        if (null != bitmap) {

            canvas.drawBitmap(bitmap, x, y, null);

        }

    }

}

精灵类主要是根据当前位置判断行走的方向,然后根据行走的方向更新精灵的位置,再绘制自身的动画。由于精灵的动画是一帧一帧的播放图片,所以这里封装了FrameAnimation.java,源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

public class FrameAnimation{

    /**动画显示的需要的资源 */

    private Bitmap[] bitmaps;

    /**动画每帧显示的时间 */

    private int[] duration;

    /**动画上一帧显示的时间 */

    protected Long lastBitmapTime;

    /**动画显示的索引值,防止数组越界 */

    protected int step;

    /**动画是否重复播放 */

    protected boolean repeat;

    /**动画重复播放的次数*/

    protected int repeatCount;

 

    /**

     * @param bitmap:显示的图片

     * @param duration:图片显示的时间

     * @param repeat:是否重复动画过程

     */

    public FrameAnimation(Bitmap[] bitmaps, int duration[], boolean repeat) {

        this.bitmaps = bitmaps;

        this.duration = duration;

        this.repeat = repeat;

        lastBitmapTime = null;

        step = 0;

    }

 

    public Bitmap nextFrame() {

        // 判断step是否越界

        if (step >= bitmaps.length) {

            //如果不无限循环

            if( !repeat ) {

                return null;

            else {

                lastBitmapTime = null;

            }

        }

 

        if (null == lastBitmapTime) {

            // 第一次执行

            lastBitmapTime = System.currentTimeMillis();

            return bitmaps[step = 0];

        }

 

        // 第X次执行

        long nowTime = System.currentTimeMillis();

        if (nowTime - lastBitmapTime <= duration[step]) {

            // 如果还在duration的时间段内,则继续返回当前Bitmap

            // 如果duration的值小于0,则表明永远不失效,一般用于背景

            return bitmaps[step];

        }

        lastBitmapTime = nowTime;

        return bitmaps[step++];// 返回下一Bitmap

    }

 

}

FrameAnimation根据每一帧的显示时间返回当前的图片帧,若没有超过指定的时间则继续返回当前帧,否则返回下一帧。
接下来需要做的是让Activty显示的View为我们之前创建的GameView,然后设置全屏显示。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public void onCreate(Bundle savedInstanceState) {

     super.onCreate(savedInstanceState);

 

     getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,

             WindowManager.LayoutParams.FLAG_FULLSCREEN);

     requestWindowFeature(Window.FEATURE_NO_TITLE);

     getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,

             WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

 

     DisplayMetrics outMetrics = new DisplayMetrics();

     this.getWindowManager().getDefaultDisplay().getMetrics(outMetrics);

     GameSurfaceView.SCREEN_WIDTH = outMetrics.widthPixels;

     GameSurfaceView.SCREEN_HEIGHT = outMetrics.heightPixels;

     GameSurfaceView gameView = new GameSurfaceView(this);

     setContentView(gameView);

 }

现在运行Android工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。

你可能感兴趣的:(android)