通过用户交互绘制到 canvas 有点滞后
Drawing to canvas with user interaction is a bit laggy
我开始用Android学习canvas绘图,我想做一个简单的应用程序。
在应用程序启动时,所谓的 'snake' 开始在屏幕上移动,当用户点击屏幕时,'snake' 会改变方向。
我很容易做到这一点,但有一个小问题:
当用户点击屏幕时,蛇 有时会在特定时刻改变方向,有时会在几毫秒后改变方向。 因此用户可以清楚地感觉到交互是 反应不如应有,蛇转动的确切时刻是相当难以预测的,即使您非常专注。一定有其他方法比我做得更好。
请检查我的代码,我使用一个带有 Runnable 的 Handler 来移动蛇。(在 canvas 上绘制并每次将其设置为背景视图,即每次使用 setContentView 设置为我的 Activity.
代码:
public class MainActivity extends Activity implements View.OnClickListener {
Paint paint = new Paint();
Canvas canvas;
View contentView; ///<The view to set by setContentView
int moveSize; ///<Sze in px to move drawer to draw another square
int leftIndex; ///<Indexes for square drawing
int topIndex;
int rightIndex;
int bottomIndex;
int maxBoundsX; ///<The max number of squares in X axis
int maxBoundsY; ///<The max number of squares in Y axis
int moveSpeedInMillis = 25; ///<One movement square per milliseconds
Bitmap bitmapToDrawOn; ///<We draw the squares to this bitmap
Direction currentDirection = Direction.RIGHT; ///< RIGHT,LEFT,UP,DOWN directions. default is RIGHT
Handler handler = new Handler(); ///<Handler for running a runnable that actually 'moves' the snake by drawing squares to shifted positions
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.i("runnable", "ran");
//Drawing a square to the current destination
drawRectPls(currentDirection);
//After some delay we call again and again and again
handler.postDelayed(runnable, moveSpeedInMillis);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*getting area properties like moveSize, and bounds*/
moveSize = searchForOptimalMoveSize();
maxBoundsX = ScreenSizer.getScreenWidth(this) / moveSize;
maxBoundsY = ScreenSizer.getScreenHeight(this) / moveSize;
Log.i("moveSize", "moveSize: " + moveSize);
Log.i("maxBounds: ", "x: " + maxBoundsX + " ; " + "y: " + maxBoundsY);
/*setting start pos*/
//We start on the lower left part of the screen
leftIndex = moveSize * (-1);
rightIndex = 0;
bottomIndex = moveSize * maxBoundsY;
topIndex = moveSize * (maxBoundsY - 1);
//Setting contentView, bitmap, and canvas
contentView = new View(this);
contentView.setOnClickListener(this);
bitmapToDrawOn = Bitmap.createBitmap(ScreenSizer.getScreenWidth(this), ScreenSizer.getScreenHeight(this), Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmapToDrawOn);
contentView.setBackground(new BitmapDrawable(getResources(), bitmapToDrawOn));
setContentView(contentView);
/*starts drawing*/
handler.post(runnable);
}
/**
* Draws a square to the next direction
*
* @param direction the direction to draw the next square
*/
private void drawRectPls(Direction direction) {
if (direction.equals(Direction.RIGHT)) {
leftIndex += moveSize;
rightIndex += moveSize;
} else if (direction.equals(Direction.UP)) {
topIndex -= moveSize;
bottomIndex -= moveSize;
} else if (direction.equals(Direction.LEFT)) {
leftIndex -= moveSize;
rightIndex -= moveSize;
} else if (direction.equals(Direction.DOWN)) {
topIndex += moveSize;
bottomIndex += moveSize;
}
addRectToCanvas();
contentView.setBackground(new BitmapDrawable(getResources(), bitmapToDrawOn));
Log.i("drawRect", "direction: " + currentDirection);
}
private void addRectToCanvas() {
paint.setColor(Color.argb(255, 100, 100, 255));
canvas.drawRect(leftIndex, topIndex, rightIndex, bottomIndex, paint);
}
/**
* Upon tapping the screen the the snake is changing direction, one way simple interaction
*/
@Override
public void onClick(View v) {
if (v.equals(contentView)) {
if (currentDirection.equals(Direction.RIGHT)) {
currentDirection = Direction.UP;
} else if (currentDirection.equals(Direction.UP)) {
currentDirection = Direction.LEFT;
} else if (currentDirection.equals(Direction.LEFT)) {
currentDirection = Direction.DOWN;
} else if (currentDirection.equals(Direction.DOWN)) {
currentDirection = Direction.RIGHT;
}
}
}
/**
* Just getting the size of a square. Searching for an integer that divides both screen's width and height
* @return
*/
private int searchForOptimalMoveSize() {
int i;
for (i = 16; i <= 64; i++) {
Log.i("iter", "i= " + i);
if (ScreenSizer.getScreenWidth(this) % i == 0) {
Log.i("iter", ScreenSizer.getScreenWidth(this) + "%" + i + " =0 !");
if (ScreenSizer.getScreenHeight(this) % i == 0) {
Log.i("iter", ScreenSizer.getScreenHeight(this) + "%" + i + " =0 !");
return i;
}
}
}
return -1;
}
/**
* Stops the handler
*/
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacks(runnable);
}
}
编辑:
我已经修改了我的代码,现在 视图包含所有细节 我使用 onDraw 和 invalidate 方法就像菲利普建议的那样。
结果好一点,但我仍然可以清楚地感觉到用户交互导致方向变化滞后。
也许我应该用线程做些什么?
public class SpiralView extends View implements View.OnClickListener {
int leftIndex; ///<Indexes for square drawing
int topIndex;
int rightIndex;
int bottomIndex;
int speedInMillis = 50;
int moveSize; ///<Sze in px to move drawer to draw another square
int maxBoundsX; ///<The max number of squares in X axis
int maxBoundsY; ///<The max number of squares in Y axis
Paint paint = new Paint();
Direction currentDirection = Direction.RIGHT; ///< RIGHT,LEFT,UP,DOWN directions. default is RIGHT
public void setUp(int moveSize, int maxBoundsX, int maxBoundsY) {
this.moveSize = moveSize;
this.maxBoundsX = maxBoundsX;
this.maxBoundsY = maxBoundsY;
this.leftIndex = moveSize * (-1);
this.rightIndex = 0;
this.bottomIndex = moveSize * (maxBoundsY);
this.topIndex = moveSize * (maxBoundsY - 1);
}
public SpiralView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnClickListener(this);
}
/**
* Draws a square to the next direction
*
* @param direction the direction to draw the next square
*/
private void drawOnPlease(Direction direction, Canvas canvas) {
if (direction.equals(Direction.RIGHT)) {
leftIndex += moveSize;
rightIndex += moveSize;
} else if (direction.equals(Direction.UP)) {
topIndex -= moveSize;
bottomIndex -= moveSize;
} else if (direction.equals(Direction.LEFT)) {
leftIndex -= moveSize;
rightIndex -= moveSize;
} else if (direction.equals(Direction.DOWN)) {
topIndex += moveSize;
bottomIndex += moveSize;
}
Log.i("drawRect", "direction: " + currentDirection);
Log.i("drawRect", "indexes: "+topIndex+" , "+rightIndex+" ," +bottomIndex+" , "+leftIndex);
addRectToCanvas(canvas);
}
private void addRectToCanvas(Canvas canvas) {
paint.setColor(Color.argb(255, 100, 100, 255));
canvas.drawRect(leftIndex, topIndex, rightIndex, bottomIndex, paint);
}
@Override
protected void onDraw(Canvas canvas) {
try {
drawOnPlease(currentDirection, canvas);
synchronized (this) {
wait(speedInMillis);
}
invalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void onClick(View v) {
if (currentDirection.equals(Direction.RIGHT)) {
currentDirection = Direction.UP;
} else if (currentDirection.equals(Direction.UP)) {
currentDirection = Direction.LEFT;
} else if (currentDirection.equals(Direction.LEFT)) {
currentDirection = Direction.DOWN;
} else if (currentDirection.equals(Direction.DOWN)) {
currentDirection = Direction.RIGHT;
}
//..?
invalidate();
}
}
不要使用 Handler 来绘制 Canvas。
相反,您应该创建一个自定义视图并使用 onDraw(Canvas canvas) 方法。
在此方法中,您可以像之前那样在 Canvas 对象上绘制。
通过调用 invalidate() 方法,您会触发一个新的 onDraw() 调用。
在 onTouch() 或 onClick() 函数中,您还可以通过调用 invalidate()
来触发新的 onDraw 调用
class SnakView extends View {
@Override
protected void onDraw(Canvas canvas) {
// draw on canvas
invalidate();
}
@Override
public void onClick(View v) {
// handle the event
invalidate();
}
}
您可以尝试将 android:hardwareAccelerated="true"
添加到您的清单,或者 .
这适用于 3.0+ 的设备。
另外你的目标 api 等级应该是 11。
这样运行起来会更顺畅
幻数是 16 毫秒,android 重绘视图时没有 framedrops。
查看来自 android 开发人员的视频,其中解释了这一点。
https://www.youtube.com/watch?v=CaMTIgxCSqU&index=25&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
特别看看这个视频
https://youtu.be/vkTn3Ule4Ps?t=54
它解释了如何使用canvas 裁剪 以便不在每个圆环中绘制整个表面,螺母只绘制需要绘制的部分。
我开始用Android学习canvas绘图,我想做一个简单的应用程序。
在应用程序启动时,所谓的 'snake' 开始在屏幕上移动,当用户点击屏幕时,'snake' 会改变方向。
我很容易做到这一点,但有一个小问题:
当用户点击屏幕时,蛇 有时会在特定时刻改变方向,有时会在几毫秒后改变方向。 因此用户可以清楚地感觉到交互是 反应不如应有,蛇转动的确切时刻是相当难以预测的,即使您非常专注。一定有其他方法比我做得更好。
请检查我的代码,我使用一个带有 Runnable 的 Handler 来移动蛇。(在 canvas 上绘制并每次将其设置为背景视图,即每次使用 setContentView 设置为我的 Activity.
代码:
public class MainActivity extends Activity implements View.OnClickListener {
Paint paint = new Paint();
Canvas canvas;
View contentView; ///<The view to set by setContentView
int moveSize; ///<Sze in px to move drawer to draw another square
int leftIndex; ///<Indexes for square drawing
int topIndex;
int rightIndex;
int bottomIndex;
int maxBoundsX; ///<The max number of squares in X axis
int maxBoundsY; ///<The max number of squares in Y axis
int moveSpeedInMillis = 25; ///<One movement square per milliseconds
Bitmap bitmapToDrawOn; ///<We draw the squares to this bitmap
Direction currentDirection = Direction.RIGHT; ///< RIGHT,LEFT,UP,DOWN directions. default is RIGHT
Handler handler = new Handler(); ///<Handler for running a runnable that actually 'moves' the snake by drawing squares to shifted positions
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.i("runnable", "ran");
//Drawing a square to the current destination
drawRectPls(currentDirection);
//After some delay we call again and again and again
handler.postDelayed(runnable, moveSpeedInMillis);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*getting area properties like moveSize, and bounds*/
moveSize = searchForOptimalMoveSize();
maxBoundsX = ScreenSizer.getScreenWidth(this) / moveSize;
maxBoundsY = ScreenSizer.getScreenHeight(this) / moveSize;
Log.i("moveSize", "moveSize: " + moveSize);
Log.i("maxBounds: ", "x: " + maxBoundsX + " ; " + "y: " + maxBoundsY);
/*setting start pos*/
//We start on the lower left part of the screen
leftIndex = moveSize * (-1);
rightIndex = 0;
bottomIndex = moveSize * maxBoundsY;
topIndex = moveSize * (maxBoundsY - 1);
//Setting contentView, bitmap, and canvas
contentView = new View(this);
contentView.setOnClickListener(this);
bitmapToDrawOn = Bitmap.createBitmap(ScreenSizer.getScreenWidth(this), ScreenSizer.getScreenHeight(this), Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmapToDrawOn);
contentView.setBackground(new BitmapDrawable(getResources(), bitmapToDrawOn));
setContentView(contentView);
/*starts drawing*/
handler.post(runnable);
}
/**
* Draws a square to the next direction
*
* @param direction the direction to draw the next square
*/
private void drawRectPls(Direction direction) {
if (direction.equals(Direction.RIGHT)) {
leftIndex += moveSize;
rightIndex += moveSize;
} else if (direction.equals(Direction.UP)) {
topIndex -= moveSize;
bottomIndex -= moveSize;
} else if (direction.equals(Direction.LEFT)) {
leftIndex -= moveSize;
rightIndex -= moveSize;
} else if (direction.equals(Direction.DOWN)) {
topIndex += moveSize;
bottomIndex += moveSize;
}
addRectToCanvas();
contentView.setBackground(new BitmapDrawable(getResources(), bitmapToDrawOn));
Log.i("drawRect", "direction: " + currentDirection);
}
private void addRectToCanvas() {
paint.setColor(Color.argb(255, 100, 100, 255));
canvas.drawRect(leftIndex, topIndex, rightIndex, bottomIndex, paint);
}
/**
* Upon tapping the screen the the snake is changing direction, one way simple interaction
*/
@Override
public void onClick(View v) {
if (v.equals(contentView)) {
if (currentDirection.equals(Direction.RIGHT)) {
currentDirection = Direction.UP;
} else if (currentDirection.equals(Direction.UP)) {
currentDirection = Direction.LEFT;
} else if (currentDirection.equals(Direction.LEFT)) {
currentDirection = Direction.DOWN;
} else if (currentDirection.equals(Direction.DOWN)) {
currentDirection = Direction.RIGHT;
}
}
}
/**
* Just getting the size of a square. Searching for an integer that divides both screen's width and height
* @return
*/
private int searchForOptimalMoveSize() {
int i;
for (i = 16; i <= 64; i++) {
Log.i("iter", "i= " + i);
if (ScreenSizer.getScreenWidth(this) % i == 0) {
Log.i("iter", ScreenSizer.getScreenWidth(this) + "%" + i + " =0 !");
if (ScreenSizer.getScreenHeight(this) % i == 0) {
Log.i("iter", ScreenSizer.getScreenHeight(this) + "%" + i + " =0 !");
return i;
}
}
}
return -1;
}
/**
* Stops the handler
*/
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacks(runnable);
}
}
编辑:
我已经修改了我的代码,现在 视图包含所有细节 我使用 onDraw 和 invalidate 方法就像菲利普建议的那样。
结果好一点,但我仍然可以清楚地感觉到用户交互导致方向变化滞后。
也许我应该用线程做些什么?
public class SpiralView extends View implements View.OnClickListener {
int leftIndex; ///<Indexes for square drawing
int topIndex;
int rightIndex;
int bottomIndex;
int speedInMillis = 50;
int moveSize; ///<Sze in px to move drawer to draw another square
int maxBoundsX; ///<The max number of squares in X axis
int maxBoundsY; ///<The max number of squares in Y axis
Paint paint = new Paint();
Direction currentDirection = Direction.RIGHT; ///< RIGHT,LEFT,UP,DOWN directions. default is RIGHT
public void setUp(int moveSize, int maxBoundsX, int maxBoundsY) {
this.moveSize = moveSize;
this.maxBoundsX = maxBoundsX;
this.maxBoundsY = maxBoundsY;
this.leftIndex = moveSize * (-1);
this.rightIndex = 0;
this.bottomIndex = moveSize * (maxBoundsY);
this.topIndex = moveSize * (maxBoundsY - 1);
}
public SpiralView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnClickListener(this);
}
/**
* Draws a square to the next direction
*
* @param direction the direction to draw the next square
*/
private void drawOnPlease(Direction direction, Canvas canvas) {
if (direction.equals(Direction.RIGHT)) {
leftIndex += moveSize;
rightIndex += moveSize;
} else if (direction.equals(Direction.UP)) {
topIndex -= moveSize;
bottomIndex -= moveSize;
} else if (direction.equals(Direction.LEFT)) {
leftIndex -= moveSize;
rightIndex -= moveSize;
} else if (direction.equals(Direction.DOWN)) {
topIndex += moveSize;
bottomIndex += moveSize;
}
Log.i("drawRect", "direction: " + currentDirection);
Log.i("drawRect", "indexes: "+topIndex+" , "+rightIndex+" ," +bottomIndex+" , "+leftIndex);
addRectToCanvas(canvas);
}
private void addRectToCanvas(Canvas canvas) {
paint.setColor(Color.argb(255, 100, 100, 255));
canvas.drawRect(leftIndex, topIndex, rightIndex, bottomIndex, paint);
}
@Override
protected void onDraw(Canvas canvas) {
try {
drawOnPlease(currentDirection, canvas);
synchronized (this) {
wait(speedInMillis);
}
invalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void onClick(View v) {
if (currentDirection.equals(Direction.RIGHT)) {
currentDirection = Direction.UP;
} else if (currentDirection.equals(Direction.UP)) {
currentDirection = Direction.LEFT;
} else if (currentDirection.equals(Direction.LEFT)) {
currentDirection = Direction.DOWN;
} else if (currentDirection.equals(Direction.DOWN)) {
currentDirection = Direction.RIGHT;
}
//..?
invalidate();
}
}
不要使用 Handler 来绘制 Canvas。 相反,您应该创建一个自定义视图并使用 onDraw(Canvas canvas) 方法。 在此方法中,您可以像之前那样在 Canvas 对象上绘制。 通过调用 invalidate() 方法,您会触发一个新的 onDraw() 调用。 在 onTouch() 或 onClick() 函数中,您还可以通过调用 invalidate()
来触发新的 onDraw 调用class SnakView extends View {
@Override
protected void onDraw(Canvas canvas) {
// draw on canvas
invalidate();
}
@Override
public void onClick(View v) {
// handle the event
invalidate();
}
}
您可以尝试将 android:hardwareAccelerated="true"
添加到您的清单,或者 .
这适用于 3.0+ 的设备。
另外你的目标 api 等级应该是 11。
这样运行起来会更顺畅
幻数是 16 毫秒,android 重绘视图时没有 framedrops。
查看来自 android 开发人员的视频,其中解释了这一点。 https://www.youtube.com/watch?v=CaMTIgxCSqU&index=25&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
特别看看这个视频 https://youtu.be/vkTn3Ule4Ps?t=54
它解释了如何使用canvas 裁剪 以便不在每个圆环中绘制整个表面,螺母只绘制需要绘制的部分。