View’s twin brother-SurfaceView
Overview
The Android system provides View for drawing processing. View can meet most of the drawing needs, but it is not enough at some times, especially when some development is carried out.
General View redraws the view by refreshing. The Android system redraws the screen by sending out the VSYNC signal. The refresh interval is 16ms.
If the View completes the operation that needs to be performed within 16ms, then the user will not feel stuck visually; and if the execution logic is too much, especially the interface that needs to be refreshed frequently, Such as the game interface, then it will continue to block the main thread, resulting in interface freezes.
We can often see in the log
SKipped 47 frames! The application may be doing too much work on its main Thread
Many reasons for this kind of alarm are caused by too much processing logic in the drawing process.
In order to avoid this problem, Android provides SurfaceView to solve this problem.
The difference between SurfaceView and View
The difference between SurfaceView and View:
- View is mainly applicable In the case of active update, SurfaceView is suitable for passive update, such as frequent refresh.
- View performs the screen in the main thread, while SurfaceView usually refreshes the page in a sub-thread
- View does not use double buffering mechanism when drawing, while SurfaceView is at the bottom A double buffering mechanism is implemented
In short, if your custom View needs to be refreshed frequently, or the amount of data processed during refresh is relatively large, you can consider using SurfaceView instead of View.
Use of SurfaceView
Although the use of SurfaceView is more complicated than that of View, there is a set of template codes used when SurfaceView is used. Most All SurfaceView drawing operations can be written using such template code.
Usually, use the following steps to create a SurfaceView template
Create SurfaceView
Create a custom SurfaceView inherited from SurfaceView , And implement two interfaces: SurfaceHolder.Callback and Runnable.
The code is as follows:
For the SurfaceHolder.Callback interface
public < span class="hljs-class">class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder. Callback ,Runnable { .......}
To implement the SurfaceHolder.Callback and Runnable interfaces, the following methods need to be rewritten
/** * SurfaceView creation* @param holder */ @Override public void surfaceCreated(SurfaceHolder holder) {} /** * SurfaceView change* @param holder * @param format * @param width * @param height */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} /* * * SurfaceView destruction* @param holder */ @Override public void surfaceDestroyed(SurfaceHolder holder) {}
< p>For the Runnable interface, there is no need to say more, you need to rewrite the run method
@Override public void run() {}
< h3 id="Initialize surfaceview">Initialize SurfaceView
In the construction method of custom SurfaceView, SurfaceView needs to be initialized.
Generally, we usually define the following three member variables
//SurfaceHolder private SurfaceHolder mSurfaceHolder; // Canvas for drawing private Canvas mCanvas; // Sub-thread flag bit private boolean span> mIsDrawing;
The initialization method is to initialize the SurfaceHolder, and then use the following code to initialize a SurfaceHolder object and register the SurfaceHolder callback method.
mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this);< /span>
The other two member variables mCanvas and flag bits.
Canvas needless to say, it is the same as using Canvas drawing in the onDraw method of View.
Another flag bit is used to control the child thread. SurfaceView usually starts a child thread to draw, and this flag bit can control the child thread.
Use SurfaceView
Through the lockCanvas() method of the SurfaceHolder object, you can get the current Canvas drawing object.
However, it should be noted that the obtained Canvas object is still the last Canvas object, not a new object. Therefore, the previous drawing operations will be retained. If you need to erase, you can use the drawColor() method to clean the screen before drawing.
When drawing, make full use of the three callback methods of SurfaceView, open the child thread in the surfaceCreated method for drawing,
and the child thread uses a while( mIsDrawing) loop to draw continuously, and submit the canvas content through the unlockCanvasAndPost(mCanvas) method.
The template code of the entire SurfaceView is as follows:
import android.content.Context; import android.graphics.Canvas;import android.util.AttributeSet;import android.view.SurfaceHolder;import android.view.SurfaceView;/** * MyApp * * @author Mr.Yang on 2016-04-22 15:59. * @version 1.0 *< span class="hljs-javadoctag"> @desc */public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback span>, Runnable { //SurfaceHolder private SurfaceHolder mSurfaceHolder; // Canvas for drawing private Canvas mCanvas; // Sub-thread flag private boolean mIsDrawing; /** * Constructor* * @param context */ public SurfaceViewTemplate(Context context) {super(context); initView();} /** * Constructor* * @param context * @param attrs */ public SurfaceViewTemplate(Context context, AttributeSet attrs) {super(context, attrs); initView();} /** * Constructor* * @param context * @param attrs * @param span> defStyleAttr */ public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr); initView();} /** * Initialization, call in the constructor*/ private void < span class="hljs-title">initView() {mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); setFocusable(true); setFo cusableInTouchMode(true); this.setKeepScreenOn(true span>); //mSurfaceHolder.setFormat(PixelFormat.OPAQUE);} /** * SurfaceView creation* * @param holder */ @Override public span> void surfaceCreated(SurfaceHolder holder) {mIsDrawing = true ; new Thread(this).start();} /** * SurfaceView changes* * @param holder * @param format * @param width * @param he ight */ @Override public void< /span> surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} /** * SurfaceView destruction* * @param holder */ @Override public void surfaceDestroyed(SurfaceHolder holder) {mIsDrawing = false; } @Override public void run() {while (mIsDrawing) {draw() ;}} /** * */ private void< /span> draw() {try {mCanvas = mSurfaceHolder.lockCanvas(); // draw something} catch (Exception e) {} finally {if (mCanvas != null) {mSurfaceHolder.unlockCanvasAndPost(mCanvas);}} }} pre> The above code can basically meet most of the SurfaceView drawing needs. It should be noted that put mSurfaceHolder.unlockCanvasAndPost(mCanvas) in finally to ensure that the content can be submitted every time p>
SurfaceView example
sine curve
To draw a sine curve, you only need to constantly change the horizontal The value of the ordinate, and let them satisfy the sine function, so a Path object is used to save the coordinate points on the sine function, and the value of the abscissa and ordinate is constantly changed in the sub-thread While loop.
import android.content.Context;import android. graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.util.AttributeSet;import< /span> android.view.SurfaceHolder;import android.view.SurfaceView;/** * MyApp * * @author Mr.Yang on 2016-04-22 22:17. * @version 1.0 * @desc */public class SinView extends SurfaceView implements SurfaceHolder.Callback , Runnable { private SurfaceHolder mHolder; private Canvas mCanvas; private boolean mIsDrawing; private int x = 0; private span> int y = 0; private Path mPath; private Paint mPaint; public SinView(Context context) {super(context); initView();} publi c SinView(Context context, AttributeSet attrs) {super(context, attrs); initView ();} public SinView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle); initView();} private span> void initView() {mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); mPath = new span> Path(); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10); // Set the brush width mPaint.setStrokeCap(Paint.Cap.ROUND); // Set the corner to be rounded mPaint.setStrokeJoin(Paint.Join.ROUND);//The joint is rounded} @Override public void surfaceCreated (SurfaceHolder holder) {mIsDrawing = true; mPath.moveTo(0, 400); new Thread(this).start( );} @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceHolder holder) {mIsDrawing = false;} @Override public void run() {while (mIsDrawing) { draw(); x += 1; y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400); mPath.lineTo(x, y);}} private void draw() {try {mCanvas = mHolder.lockCanvas(); // SurfaceView background mCanvas.drawColor(Color.WHITE); mCanvas.drawPath(mPath , mPaint);} catch (Exception e) {} finally {if (mCanvas != null) mHolder.unlockCanvasAndPost(mCanvas);} }}
< span class="hljs-keyword">import
android.support.v7.app.AppCompatActivity;import android.os.Bundle;public class SinViewAct extends< /span> AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) {super .onCreate(savedInstanceState); setContentView(new SinView(this)); }}< /pre> The final look:
Drawing board h3>
Record the path of the finger sliding through the Path object for drawing, and record the Path path in the onTouchEvent() of the SurfaceView.
import android.content.Context;import android. graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.util.AttributeSet;import< /span> android.view.MotionEvent;import android.view.SurfaceHolder;import android.view.SurfaceView ;/** * MyApp * * @author Mr.Yang on 2016-04-22 22:02. * @version 1.0 * @desc */public< /span> class SimpleDraw extends SurfaceView implements SurfaceHolder.Callback, Runnable { //SurfaceHolder private SurfaceHolder mSurfaceHolder; // Canvas for drawing private Canvas mCanvas; // Child thread flag private boolean mIsDrawing; private Path mPath; private Paint mPaint; /** * Constructor* * @param context */ public SimpleDraw(Context context) {super(context); initView();} /** * Constructor* * @param span> context * @param attrs */ public SimpleDraw(Context context, AttributeSet attrs) {super(context, attrs); initView();} /** * Constructor* * @param context * @param attrs * @param defStyleAttr */ public SimpleDraw(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr); initView(); } /** * Initialization, call in the constructor */ private void initView() {mSurfaceHolder = getHolder( ); mSurfaceHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); //mSurfaceHolder.setFormat(PixelFormat.OPAQUE); mPath = new Path(); mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10.0 span>f);} /** * SurfaceView creation* * @param holder */ @Override public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; new Thread(this ).start();} /** * SurfaceView change* * @param holder * @param format * @param width * @param height */ span> @Override public void surfaceChanged(SurfaceHolder holder, int format, int width , int height) {} /** * SurfaceView destruction* * @param holder */ @Override public void surfaceDestroyed(SurfaceHolder holder) {mIsDrawing = false< /span>;} @Override public void run() {long start = System.currentTimeMillis(); while (mIsDrawing) {draw();} long end = System.currentTimeMillis(); / / 50-100 if (end-start <100) {try {Thread.sleep(100-(end-start));} catch (InterruptedException e) {e.printStackTrace();}}} /** * */ private void draw() {try {mCanvas = mSurfaceHolder.lockCanvas(); // draw something mCanvas.drawColor(Color.WHITE); mCanvas.drawPath(mPath, mPaint);} catch (Exception e) {} finally {if (mCanvas != null) {mSurfaceHolder. unlockCanvasAndPost(mCanvas);}}} @Override public boolean onTouchEvent(MotionEvent event) {int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) {case MotionEvent.ACTION_DOWN: mPath.moveTo(x, y); break; case MotionEvent.ACTION_MOVE: mPath.lineTo(x, y); break; case MotionEvent.ACTION_UP: break;} return true; }}
Sometimes the drawing is not so frequent, so we can perform Sleep operation in the sub-thread, in order to save the system as much as possible resource.
Determine the value of Sleep by judging the logical duration used by the draw method. This is a very general solution. The 100ms in the code is a rough empirical value, generally around 50ms~100ms
p>
import android.os.Bundle;import android.support. v7.app.AppCompatActivity;public class SimpleDrawAct extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(new SimpleDraw(this)); }}
By custom view Realizing the drawing board
It can also be realized by customizing View, the code is as follows
import android.content.Context; import android.graphics.Bitmap; import android.graphics .Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * MyApp * * @author Mr.Yang on 2016-04-23 10:32. * @version 1.0 * Analysis: * Place the drawing board where? * In View, we customize a View and complete the drawing in onDraw(). In addition, the View also has an onTouchEvent method, we can get the user's gesture operation! * What do I need to prepare? * A paintbrush (Paint), a canvas (Canvas), and a path (Path) to record the user's drawing route; * In addition, when drawing a line, each time it is from the point of the last drag time to the current drag time The point of occurrence! * 那么之前绘制的 就会丢失,为了保存之前绘制的内容, * 我们可以引入所谓的"双缓冲"技术: 其实就是每次不是直接绘制到Canvas上,而是先绘制到Bitmap上,等Bitmap上的绘制完了, 再一次性地绘制到View上! * * 具体的实现流程? * 初始化画笔,设置颜色等等一些参数; * 在View的onMeasure()方法中创建一个View大小的Bitmap, * 同时创建一个Canvas;onTouchEvent中获得X,Y坐标,做绘制连线, * 最后invalidate()重绘,即调用 onDraw方法将bitmap的东东画到Canvas上! */ public class SimpleDrawByCustomView extends View { // 定义画布 private Canvas mCanvas; // 定义画笔 private Paint mPaint; // 记录用户绘制的Path private Path mPath; //缓存绘制的内容 private Bitmap mBitmap; private int mLastX; private int mLastY; /** * @param context */ public S impleDrawByCustomView(Context context) { super(context); init(); } /** * @param context * @param attrs */ public SimpleDrawByCustomView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * @param context * @param attrs * @param defStyleAttr */ public SimpleDrawByCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化工作,在构造函数中调用 */ private void init() { mPaint = new Paint(); mPath = new Path(); mPaint.setColor(Color.BLUE); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.ROUND); //结合处为圆角 mPaint.setStrokeCap(Paint.Cap.ROUND); // 设置转弯处为圆角 mPaint.setStrokeWidth(20); // 设置画笔宽度 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super span>.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMeasuredWidth(); int height = getMeasuredHeight(); // 初始化bitmap,Canvas mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); } //重写该方法,在这里绘图 @Override protected void onDraw(Canvas canvas) { drawPath(); canvas.drawBitmap(mBitmap, 0, 0, null); } //绘制线条 private void drawPath() { mCanvas.drawPath(mPath, mPaint); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastX = x; mLastY = y; mPath.moveTo(mLastX, mLastY); break; case MotionEvent.ACTION_MOVE: int dx = Math.abs(x - mLastX); int dy = Math.abs(y - mLastY); if (dx > 3 || dy > 3) mPath.lineTo(x, y); mLastX = x; mLastY = y; break; } invalidate(); return true; } }
View之孪生兄弟-SurfaceView
概述
Android系统中提供了View进行绘图处理,View可以满足大部分的绘图需求,但是在某些时候却力不从心,特别是进行一些开发的时候。
一般的View通过刷新来重绘视图,Android系统通过发出VSYNC信号来进行屏幕的重绘,刷新的时间间隔是16ms。
如果在16ms内View完成了所需要执行的操作,那么用户在视觉上就不会产生卡顿的感觉;而如果执行的逻辑太多,特别是需要频繁刷新的界面,如游戏界面,那么就会不断的阻塞主线程,从而导致界面卡顿。
我们经常在log中可以看到
SKipped 47 frames! The application may be doing too much work on its main Thread
这种告警产生的很多原因就是在绘制的过程中,处理逻辑太多造成的。
为了避免这种问题,Android提供了SurfaceView来解决这个问题。
SurfaceView和View的区别
SurfaceView和View的区别:
- View主要适用于主动更行的情况,而SurfaceView适用于被动更新,例如频繁的刷新。
- View在主线程中对画面进行,而SurfaceView通常会在一个子线程中进行页面的刷新
- View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现了双缓冲机制
总之,如果你的自定义View需要频繁刷新,或者刷新时处理的数据量比较大时,就可以考虑使用SurfaceView替代View了。
SurfaceView的使用
SurfaceView的使用虽然比View要复杂,但是SurfaceView在使用时有一套使用的模板代码,大部分的SurfaceView绘图操作都可以套用这样的模板代码来进行编写。
通常情况下,使用以下步骤来创建一个SurfaceView的模板
创建SurfaceView
创建自定义的SurfaceView 继承自 SurfaceView,并且实现两个接口:SurfaceHolder.Callback 和 Runnable。
代码如下:
对于SurfaceHolder.Callback接口
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback ,Runnable { .......}
实现SurfaceHolder.Callback和Runnable接口,需要重写以下方法
/** * SurfaceView创建 * @param holder */ @Override public void surfaceCreated(SurfaceHolder holder) { } /** * SurfaceView改变 * @param holder * @param format * @param width * @param height */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } /** * SurfaceView销毁 * @param holder */ @Override public void surfaceDestroyed(SurfaceHolder holder) { }
对于Runnable接口,无需多说,需要重写run方法
@Override public void run() { }
初始化SurfaceView
在自定义的SurfaceView的构造方法中,需要对SurfaceView进行初始化。
一般我们通常定义以下三个成员变量
//SurfaceHolder private SurfaceHolder mSurfaceHolder; // 用于绘图的Canvas private Canvas mCanvas; // 子线程标志位 private boolean mIsDrawing;
初始化方法就是对SurfaceHolder进行初始化,然后通过以下代码来初始化一个SurfaceHolder对象,并注册SurfaceHolder的回调方法。
mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this);
另外两个成员变量mCanvas和标志位。
Canvas不必多说,和在View的onDraw方法中使用Canvas绘图一样。
另外一个标志位,则是用来控制子线程的。 SurfaceView通常会起一个子线程来进行绘制,而这个标志位就可以控制子线程。
使用SurfaceView
通过SurfaceHolder对象的lockCanvas()方法,就可以获取当前的Canvas绘图对象。
不过需要注意的是,获取到的Canvas对象还是继续上次的Canvas对象,而不是一个新的对象。因此,之前的绘图操作将会被保留,如果需要擦除,则可以在绘制前,通过drawColor()方法来机型清屏操作。
绘制的时候,充分利用SurfaceView的三个回调方法,在surfaceCreated方法中开启子线程进行绘制,
而子线程使用一个while(mIsDrawing)的循环来不停的绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交。
整个SurfaceView的模板代码如下:
import android.content.Context;import android.graphics.Canvas;import android.util.AttributeSet;import android.view.SurfaceHolder;import android.view.SurfaceView;/** * MyApp * * @author Mr.Yang on 2016-04-22 15:59. * @version 1.0 * @desc */public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable { //SurfaceHolder private SurfaceHolder mSurfaceHolder; // 用于绘图的Canvas private Canvas mCanvas; // 子线程标志位 private boolean mIsDrawing; /** * 构造函数 * * @param context */ public SurfaceViewTemplate(Context context) { super(context); initView(); } /** * 构造函数 * * @param context * @param attrs */ public SurfaceViewTemplate(Context co ntext, AttributeSet attrs) { super(context, attrs); initView(); } /** * 构造函数 * * @param context * @param attrs * @param defStyleAttr */ public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } /** * 初始化,在构造函数中调用 */ private void initView() { mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); setFocusable(true); setFocus ableInTouchMode(true); this.setKeepScreenOn(true); //mSurfaceHolder.setFormat(PixelFormat.OPAQUE); } /** * SurfaceView创建 * * @param holder */ @Override public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; new Thread(this).start(); } /** * SurfaceView改变 * * @param holder * @param format * @param width * @param heigh t */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } /** * SurfaceView销毁 * * @param holder */ @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsDrawing = false; } @Override public void run() { while (mIsDrawing) { draw(); } } /** * */ private void draw() { try { mCanvas = mSurfaceHolder.lockCanvas(); // draw something } catch (Exception e) { } finally { if (mCanvas != null) { mSurfaceHolder.unlockCanvasAndPost(mCanvas); } } }}
以上的代码基本可以满足大部分的SurfaceView绘图需求,需要注意的是 将mSurfaceHolder.unlockCanvasAndPost(mCanvas)放到finally中确保每次都能讲内容提交
SurfaceView实例
正弦曲线
要绘制一个正弦曲线,只需要不断地改变横纵坐标的值,并让他们满足正弦函数即可,因此使用一个Path对象来保存正弦函数上的坐标点,在子线程While循环中,不断的改变横纵坐标的值。
import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.util.AttributeSet;import android.view.SurfaceHolder;import android.view.SurfaceView;/** * MyApp * * @author Mr.Yang on 2016-04-22 22:17. * @version 1.0 * @desc */public class SinView extends SurfaceView< /span> implements SurfaceHolder.Callback, Runnable { private SurfaceHolder mHolder; private Canvas mCanvas; private boolean mIsDrawing; private int x = 0; private int y = 0; private Path mPath; private Paint mPaint; public SinView(Context context) { super(context); initView(); } public SinView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public SinView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } private void initView() { mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); mPath = new Path(); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10); // 设置画笔宽度 mPaint.setStrokeCap(Paint.Cap.ROUND); // 设置转弯处为圆角 mPaint.setStrokeJoin(Paint.Join.ROUND);//结合处为圆角 } @Override public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; mPath.moveTo(0, 400); new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsDrawing = false; } @Override public void run() { while (mIsDrawing) { draw(); x += 1; y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400); mPath.lineTo(x, y); } } private void draw() { try { mCanvas = mHolder.lockCanvas(); // SurfaceView背景 mCanvas.drawColor(Color.WHITE); mCanvas.drawPath(mPath, mPaint); } catch (Exception e) { } finally { if (mCanvas != null) mHolder.unlockCanvasAndPost(mCanvas); } }}
import android.support.v7.app.AppCompatActivity;import android.os.Bundle;public class SinViewAct extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new SinView(this)); }}
最后的样子:
绘图板
通过Path对象记录手指滑动的路径来进行绘图,在SurfaceView的onTouchEvent()中记录Path路径。
import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.SurfaceHolder;import android.view.SurfaceView;/** * MyApp * * @author Mr.Yang on 2016-04-22 22:02. * @version 1.0 * @desc */public class SimpleDraw extends SurfaceView implements SurfaceHolder.Callback, Runnable { //SurfaceHolder private SurfaceHolder mSurfaceHolder; // 用于绘图的Canvas private Canvas mCanvas; // 子线程标志位 private boolean mIsDrawing; private Path mPath; private Paint mPaint; /** * 构造函数 * * @param context */ public SimpleDraw(Context context) { super(context); initView(); } /** * 构造函数 * * @param context * @param attrs */ public SimpleDraw(Context context, AttributeSet attrs) { super(context, attrs); initView(); } /** * 构造函数 * * @param context * @param attrs * @param defStyleAttr */ public SimpleDraw(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } /** * 初始化,在构造函数中调用 */ private void initView() { mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); //mSurfaceHolder.setFormat(PixelFormat.OPAQUE); mPath = new Path(); mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10.0f); } /** * SurfaceView创建 * * @param holder */ @Override < span class="hljs-keyword">public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; new Thread(this).start(); } /** * SurfaceView改变 * * @param holder * @param format * @param width * @param height */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } /** * SurfaceView销毁 * * @param holder */ @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsDrawing = false; } @Override public void run() { long start = System.currentTimeMillis(); while (mIsDrawing) { draw(); } long end = System.currentTimeMillis(); // 50 - 100 if (end - start < 100) { try { Thread.sleep(100 - (end - start)); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * */ private void draw() { try { mCanvas = mSurfaceHolder.lockCanvas(); // draw something mCanvas.drawColor(Color.WHITE); mCanvas.drawPath(mPath, mPaint); } catch (Exception e) { } finally { if (mCanvas != null) { mSurfaceHolder.unlockCanvasAndPost(mCanvas); } } } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPath.moveTo(x, y); break; case MotionEvent.ACTION_MOVE: mPath.lineTo(x, y); break; case MotionEvent.ACTION_UP: break; } return true; }}
有时候绘制也不用这么的频繁,因此我们可以在子线程中,进行Sleep操作,以便尽可能的节约系统资源.
通过判断draw方法所使用的逻辑时长来确定Sleep的值,这是一个非常通用的解决方案,代码中的100ms是一个大致的经验值,一般在50ms~100ms左右
import android.os.Bundle;import android.support.v7.app.AppCompatActivity;public class SimpleDrawAct extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new SimpleDraw(this)); }}
通过自定义View实现绘画板
也可以通过自定义View实现 ,代码如下
import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * MyApp * * @author Mr.Yang on 2016-04-23 10:32. * @version 1.0 * 分析: * 画板放在哪里? * View里,我们自定义一个View,在onDraw()里完成绘制,另外View还有个onTouchEvent的方法, 我们可以在获取用户的手势操作! * 需要准备些什么? * 一只画笔(Paint),一块画布(Canvas),一个路径(Path)记录用户绘制路线; * 另外划线的时候,每次都是从上次拖动时间的发生点到本次拖动时间的发生点! * 那么之前绘制的 就会丢失,为了保存之前绘制的内容, * 我们可以引入所谓的"双缓冲"技术: 其实就是每次不是直接绘制到Canvas上,而是先绘制到Bitmap上,等Bitmap上的绘制完了, 再一次性地绘制到View上! * * 具体的实现流程? * 初始化画笔,设置颜色等等一些参数; * 在View的onMeasure()方法中创建一个View大小的Bitmap, * 同时创建一个Canvas;onTouchEvent中获得X,Y坐标,做绘制连线, * 最后invalidate()重绘,即调用 onDraw方法将bitmap的东东画到Canvas上! */ public class SimpleDrawByCustomView extends View { // 定义画布 private Canvas mCanvas; // 定义画笔 private Paint mPaint; // 记录用户绘制的Path private Path mPath; //缓存绘制的内容 private Bitmap mBitmap; private int mLastX; private int mLastY; /** * @param context */ public S impleDrawByCustomView(Context context) { super(context); init(); } /** * @param context * @param attrs */ public SimpleDrawByCustomView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * @param context * @param attrs * @param defStyleAttr */ public SimpleDrawByCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化工作,在构造函数中调用 */ private void init() { mPaint = new Paint(); mPath = new Path(); mPaint.setColor(Color.BLUE); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.ROUND); //结合处为圆角 mPaint.setStrokeCap(Paint.Cap.ROUND); // 设置转弯处为圆角 mPaint.setStrokeWidth(20); // 设置画笔宽度 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super span>.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMeasuredWidth(); int height = getMeasuredHeight(); // 初始化bitmap,Canvas mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); } //重写该方法,在这里绘图 @Override protected void onDraw(Canvas canvas) { drawPath(); canvas.drawBitmap(mBitmap, 0, 0, null); } //绘制线条 private void drawPath() { mCanvas.drawPath(mPath, mPaint); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastX = x; mLastY = y; mPath.moveTo(mLastX, mLastY); break; case MotionEvent.ACTION_MOVE: int dx = Math.abs(x - mLastX); int dy = Math.abs(y - mLastY); if (dx > 3 || dy > 3) mPath.lineTo(x, y); mLastX = x; mLastY = y; break; } invalidate(); return true; } }