SurfaceView 实现高性能的绘制

本文探讨了自定义View绘图的不足,如缺乏双缓冲等,指出SurfaceView在高性能绘图,尤其是游戏场景中的优势。SurfaceView通过SurfaceHolder进行后台线程绘图,提供lockCanvas方法来提高画面更新速度。示例展示了如何在SurfaceView上实现图片绘制和清除功能,适合动画元素多且需要定时控制的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       先说说自定义 View 绘图机制的缺陷:
1. View 缺乏双缓冲机制;
2. 当程序需要更新 View 上的图片时,程序必须重绘 View 上显示的整张图片;
3. 新线程无法直接更新 View 组件。

        由于 View 存在上述的缺陷,所以通过自定义 View 来实现绘图,尤其是游戏中的绘图时性能并不好。Android 提供了一个 SurfaceView 来代替 View ,在实现游戏绘图方面,SurfaceView 比 View 更加出色。

       在 Android UI 开发中一般遵循这样的规定:不要在主线程之外的线程中修改任何与 View 相关的属性。但是 SurfaceView 和 TextureView 这两个类则不遵循这个规定,它们专门设计用来在后台线程中执行绘制命令,并将绘制内容展现在屏幕上。

       SurfaceView 一般会与 SurfaceHolder 结合使用。SurfaceView 非常独特,与传统 View 的原理有很大差异。当实例化一个 SurfaceView 时,实际上会在 View 的位置创建里一个窗口,所以实际上会有两层窗口,第二层就是 SurfaceHolder 该层位于当前窗口的下方,该层才是真正的视图层。然后 View 控件会在顶层窗口简单地“打一个洞”来透明地显示下方窗口的内容。SurfaceView 还是一种非常静态的视图,无法对动画或任何形式的变换做出很好的响应。
      
        调用 SurfaceView 的 getHolder() 方法即可获取 SurfaceView 关联的 SurfaceHolder。SurfaceHolder 提供了如下方法来获取 Canvas 对象。
------> Canvas lockCanvas():锁定整个 SurfaceView 对象,获取该 SurfaceView 上的 Cnavas。
------> Canvas lockCanvas(Rect dirty):锁定 SurfaceView 上 Rect 划分的区域,获取该 SurfaceView 上的 Canvas。

       上面的第二个方法获取指定的 Canvas 时,SurfaceView 将只对 Rect 所“圈”出来的区域进行绘图,通过这种方式可以提高画面的更新速度。

       当通过 lockCanvas() 获取指定 SurfaceView 上的 Canvas 之后,接下来程序就可以调用 Canvas 进行绘图了, Canvas 绘图完成后通过如下方法来释放绘图、提交所绘制的图形。
------> unlockCanvasAndPost(canvas)

       需要注意的是,当调用 SurfaceHolder 的 unlockCanvasAndPost() 方法之后,该方法之前所绘制的图形还处于缓冲区中,下一次 lockCanvas() 方法锁定的区域可能会 “遮挡” 它。

       选用 SurfaceView 的情况:一般来说,如果程序或游戏界面的动画元素较多时,而且很多动画元素的移动都需要通过定时器来控制,就可以考虑使用 SurfaceView,而不是 View。



      下面这个示例从其他地方摘取,只修改了其中的少许代码。该示例点击屏幕就可以在相应的位置出现图片,点击按钮则清空所有。在该示例中用户可以不限次数的单击黑色区域,不用担心内存问题,因为绘制代码只是用一张位图来绘制所有的图形。效果如下:




布局文件,content_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.crazy.surfaceviewtest.MainActivity"
    tools:showIn="@layout/activity_main">

    <Button
        android:id="@+id/button_erase"
        android:text="清除"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <SurfaceView
        android:id="@+id/surface"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>



MainActivity.java :
package com.crazy.surfaceviewtest;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements View.OnClickListener,
        View.OnTouchListener, SurfaceHolder.Callback{

    private SurfaceView mSurface;
    private DrawingThread mThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        findViewById(R.id.button_erase).setOnClickListener(this);
        mSurface = (SurfaceView)findViewById(R.id.surface);
        mSurface.setOnTouchListener(this);
        mSurface.getHolder().addCallback(this);
    }

    /**
     *  当 SurfaceView 被创建时回调该方法
     */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mThread = new DrawingThread(holder,
                BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher));
        mThread.start();
    }

    /**
     *  当一个 SurfaceView 的格式或者大小发生改变时回调该方法
     */
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        mThread.updateSize(width, height);
    }

    /**
     *  当 SurfaceView 将要被销毁时回调该方法
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mThread.quit();
        mThread = null;
    }

    @Override
    public void onClick(View v) {
        mThread.clearItems();
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mThread.addItem((int)event.getX(), (int)event.getY());
        }
        return true;
    }

    /**
     *  HandlerThread 是一个方便的框架辅助类,用来生成后台工作线程以处理收到的消息
     */
    private static class DrawingThread extends HandlerThread implements Handler.Callback {

        private static final int MSG_ADD = 100;
        private static final int MSG_MOVE = 101;
        private static final int MSG_CLEAR = 102;

        private int mDrawingWidth, mDrawingHeight;

        private SurfaceHolder mDrawingSurface;
        private Paint mPaint;
        private Handler mReceiver;
        private Bitmap mIcon;
        private ArrayList<DrawingItem> mLocations;
        // 定义一个记录图像是否开始渲染的旗帜
        private boolean mRunning;

        private class DrawingItem {
            // 当前位置的标识
            int x, y;
            // 动作方向的标识
            boolean horizontal, vertical;

            public DrawingItem(int x, int y, boolean horizontal, boolean vertical) {
                this.x = x;
                this.y = y;
                this.horizontal = horizontal;
                this.vertical = vertical;
            }
        }

        public DrawingThread(SurfaceHolder holder, Bitmap icon) {
            super("DrawingThread");
            mDrawingSurface = holder;
            mLocations = new ArrayList<>();
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mIcon = icon;
        }

        @Override
        protected void onLooperPrepared() {
            mReceiver = new Handler(getLooper(), this);
            // 开始渲染
            mRunning = true;
            mReceiver.sendEmptyMessage(MSG_MOVE);
        }

        @Override
        public boolean quit() {
            // 退出之前清除所有的消息
            mRunning = false;
            mReceiver.removeCallbacksAndMessages(null);

            return super.quit();
        }

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_ADD:
                    // 在触摸的位置创建一个新的条目,该条目的开始方向是随机的
                    DrawingItem newItem = new DrawingItem(msg.arg1, msg.arg2,
                            Math.round(Math.random()) == 0,
                            Math.round(Math.random()) == 0);
                    mLocations.add(newItem);
                    break;
                case MSG_CLEAR:
                    // 删除所有对象
                    mLocations.clear();
                    break;
                case MSG_MOVE:
                    if (!mRunning) return true;

                    // 渲染一帧
                    // 锁定 SurfaceView,并返回到要绘图的 Canvas
                    Canvas canvas = mDrawingSurface.lockCanvas();
                    if (canvas == null) {
                        break;
                    }
                    // 首先清空 Canvas
                    // 如果没有这句代码,当图标移动时会在图标之前的位置出现拖尾痕迹
                    canvas.drawColor(Color.BLACK);
                    // 绘制每个条目
                    for (DrawingItem item : mLocations) {
                        // 更新位置
                        item.x += (item.horizontal ? 5 : -5);
                        if (item.x >= (mDrawingWidth - mIcon.getWidth())) {
                            item.horizontal = false;
                        }
                        if (item.x <= 0) {
                            item.horizontal = true;
                        }
                        item.y += (item.vertical ? 5 : -5);
                        if (item.y >= (mDrawingHeight - mIcon.getHeight())) {
                            item.vertical = false;
                        }
                        if (item.y <= 0) {
                            item.vertical = true;
                        }
                        canvas.drawBitmap(mIcon, item.x, item.y, mPaint);
                    }
                    // 解锁 Canvas,并渲染当前的图像
                    mDrawingSurface.unlockCanvasAndPost(canvas);
                    break;
            }
            // 发送下一帧
            if (mRunning) {
                mReceiver.sendEmptyMessage(MSG_MOVE);
            }
            return true;
        }

        public void updateSize(int width, int height){
            mDrawingWidth = width;
            mDrawingHeight = height;
        }

        public void addItem(int x, int y) {
            // 通过 Message 参数将位置传给处理程序
            Message msg = Message.obtain(mReceiver, MSG_ADD, x, y);
            mReceiver.sendMessage(msg);
        }

        public void clearItems(){
            mReceiver.sendEmptyMessage(MSG_CLEAR);
        }
    }
}


如果没有调用 drawColor() 方法,则会出现下面的效果:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值