How to optimize drawing text on canvas

2019-06-06 09:56发布

问题:

I have window with 3 circles, they are rotating simultaneously. Everything is good until a Add text to the circles, then the rotation starts to lagging.

How can i optimize drawing on canvas ? This is my code:

@Override
protected void onDraw(final Canvas canvas) {
    if (mPaint == null) {
        mPaint = new Paint();
        mPaint.setTextSize(20f);
    }       
    drawUpperCircle(canvas);
    drawBottomCircle(canvas);
    drawMainCircle(canvas);

    try {
        Thread.sleep(1, 1);
        invalidate();
        mRotation += 0.9;
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    super.onDraw(canvas);
}
   private void drawUpperCircle(Canvas canvas) {
    canvas.save();
    canvas.rotate(mRotation, 0, mUpperCircleCentr);
    mPaint.setColor(Color.CYAN);
    canvas.drawCircle(0, mUpperCircleCentr, mUpperCirclRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, 0, mUpperCircleCentr);
        canvas.drawLine(0, mUpperCircleCentr, mUpperCirclRadius, mUpperCircleCentr, mPaint);
        //          canvas.drawText("my text" + String.valueOf(i), mUpperCirclRadius * 2 / 3, mUpperCircleCentr - 4, mPaint);
    }
    canvas.restore();
}

private void drawBottomCircle(Canvas canvas) {
    canvas.save();
    canvas.rotate(mRotation, 0, mBottomCircleCentr);
    mPaint.setColor(Color.RED);
    canvas.drawCircle(0, mBottomCircleCentr, mBottomCirclRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, 0, mBottomCircleCentr);
        canvas.drawLine(0, mBottomCircleCentr, mBottomCirclRadius, mBottomCircleCentr, mPaint);
        //          canvas.drawText("my text" + String.valueOf(i), mBottomCirclRadius * 2 / 3, mBottomCircleCentr - 4, mPaint);
    }
    canvas.restore();
}

private void drawMainCircle(Canvas canvas) {
    canvas.save();
    canvas.rotate(mRotation, 0, mMainCircleCentr);
    mPaint.setColor(Color.argb(100, 100, 100, 100));
    canvas.drawCircle(0, mMainCircleCentr, mMainCirclRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, 0, mMainCircleCentr);
        canvas.drawLine(0, mMainCircleCentr, mMainCirclRadius, mMainCircleCentr, mPaint);
        canvas.drawText("my text" + String.valueOf(i), mMainCirclRadius * 2 / 3, mMainCircleCentr - 4, mPaint);
    }
    canvas.restore();
}

EDIT To improve performance and remove drawing from UI thread I have Used Double Buffering With SurfaceView and implement @Morgans optimizations. That is how it realized.

DrawView.java

public class DrawView extends SurfaceView implements SurfaceHolder.Callback {

...............................................................

public DrawView(Context context, AttributeSet attrs) {
    super(context, attrs);
    getHolder().addCallback(this);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    float currentX = event.getX();
    float currentY = event.getY();
    float deltaX, deltaY;
    switch (event.getAction()) {
    case MotionEvent.ACTION_MOVE:
        // Modify rotational angles according to movement
        deltaX = currentX - previousX;
        deltaY = currentY - previousY;
        mDrawThread.mRotation += deltaY * 180 / getHeight();
    }
    // Save current x, y
    previousX = currentX;
    previousY = currentY;
    return true; // Event handled
}

@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {

}

@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
    mDrawThread = new DrawThread(getHolder(), this);
    mDrawThread.setRunning(true);
    mDrawThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    boolean retry = true;
    mDrawThread.setRunning(false);
    while (retry) {
        try {
            mDrawThread.join();
            retry = false;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 }

And the main work is done in the DrawThread.java

public class DrawThread extends Thread {

private ArrayList<Path> mMainCirclePaths = new ArrayList<Path>(SEG_COUNT);
private ArrayList<Path> mUpperCirclePaths = new ArrayList<Path>(SEG_COUNT);
private ArrayList<Path> mCenterCirclePaths = new ArrayList<Path>(SEG_COUNT);
private ArrayList<Path> mBottomCirclePaths = new ArrayList<Path>(SEG_COUNT);

private boolean mRun = false;
private SurfaceHolder mSurfaceHolder;
private DrawView mDrawView;
private Paint mPaint;

private CirclesModel mCirclesModel;
public float mRotation = 0;

public DrawThread(SurfaceHolder surfaceHolder, DrawView drawView) {
    mSurfaceHolder = surfaceHolder;
    mDrawView = drawView;
    mCirclesModel = new CirclesModel(mDrawView.getHeight());
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(18f);
    initPaths();
}

public void setRunning(boolean b) {
    mRun = b;
}

@Override
public void run() {
    while (mRun) {
        Canvas canvas = null;
        try {
            canvas = mSurfaceHolder.lockCanvas(null);
            synchronized (mSurfaceHolder) {
                drawMainCircle(canvas);
                mPaint.setColor(Color.WHITE);
                canvas.drawCircle(mCirclesModel.mMainCircleCentr[CirclesModel.X], mCirclesModel.mMainCircleCentr[CirclesModel.Y],
                        mCirclesModel.mSmallCirclesRadius, mPaint);
                drawCenterCircle(canvas);
                drawUpperCircle(canvas);
                drawBottomCircle(canvas);
                //mRotation += 0.5f;

            }
        } finally {
            if (canvas != null) {
                mSurfaceHolder.unlockCanvasAndPost(canvas);
            }
        }
    }
}

private void drawMainCircle(Canvas canvas) {
    canvas.save();
    canvas.rotate(mRotation, mCirclesModel.mMainCircleCentr[CirclesModel.X], mCirclesModel.mMainCircleCentr[CirclesModel.Y]);
    float rot = mRotation;
    mPaint.setColor(Color.LTGRAY/* argb(100, 255, 255, 255) */);
    canvas.drawCircle(mCirclesModel.mMainCircleCentr[CirclesModel.X], mCirclesModel.mMainCircleCentr[CirclesModel.Y],
            mCirclesModel.mBigCirclesRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, mCirclesModel.mMainCircleCentr[CirclesModel.X], mCirclesModel.mMainCircleCentr[CirclesModel.Y]);
        rot += SEG_IN_GRAD;
        float absRot = Math.abs(rot % 360);
        if (absRot > mCirclesModel.mMainCircleSegment[0] && absRot < mCirclesModel.mMainCircleSegment[1]) {
            continue;
        }
        canvas.drawLine(mCirclesModel.mMainCircleCentr[CirclesModel.X], mCirclesModel.mMainCircleCentr[CirclesModel.Y],
                mCirclesModel.mBigCirclesRadius, mCirclesModel.mMainCircleCentr[CirclesModel.Y], mPaint);
        canvas.drawPath(mMainCirclePaths.get(i), mPaint);
        // canvas.drawText("my text" + String.valueOf(i),
        // mMainCirclRadius * 2 / 3, mMainCircleCentr - 4, mPaint);
    }
    canvas.restore();
}
   .................................................................
}

Double Buffering is implemented in the two lines of code

canvas = mSurfaceHolder.lockCanvas(null); here I take from surface view canvas in which i will draw next frame.

mSurfaceHolder.unlockCanvasAndPost(canvas); here I am overlaping current image on SurfaceView with new canwas, this is the moment where image changes. Be aware if you have transparent elements then the previous image will be still visible, Image is not replaced, but overlaped.

回答1:

Below is a version of your code that contains a few optimizations.

First, I try not to draw the lines and text that currently offscreen. I do this by tracking the rotation angle, and skipping the drawing for net rotations between 90 and 270 degrees. On my 2.3 simulator this improved performance overall by 25%.

Second, I "cache" the strings I am going to draw by initializing an array (ArrayList<Path>) with one Path for each string I need to draw. I do this in the same place you were one-time initializing the mPaint. Then I draw the strings using canvas.drawPath(...). On my 2.3 simulator this improved performance by another 33%. The net effect was to about double the rotation speed. Also, it stopped the text from "wiggling around".

A few other notes:

I removed the Thread.sleep(1,1). Not sure exactly what you were trying to accomplish with that.

I changed rotation delta to 1.0 from 0.9. Not sure why you were using 0.9. Note that if you change to back, my "log time it takes to rotate 10 degrees" will not quite work since mRotation % 10 may seldom be 0.

On a 4.1 simulator, the rotation was generally much faster (about 4x) than on my 2.3 simulator. And a 4.1 device was faster yet.

public class AnimView extends View {
Paint mPaint;
ArrayList<Path> mTextPaths;

float mRotation = 0f;

float mUpperCircleCentr = 150f;
float mUpperCirclRadius = 150f;

private static final int SEG_COUNT = 60;
private static final float SEG_IN_GRAD = 360.0f / SEG_COUNT;

float mBottomCircleCentr = 450f;
float mBottomCirclRadius = 150f;

float mMainCircleCentr = 300f;
float mMainCirclRadius = 300f;

long mLastMillis = 0L;

// ctors removed

@Override
protected void onDraw(final Canvas canvas) {
    super.onDraw(canvas);

    if (mPaint == null) {
        mPaint = new Paint();
        mPaint.setTextSize(20f);

        // init text paths
        mTextPaths = new ArrayList<Path>(SEG_COUNT);
        for (int i = 0; i < SEG_COUNT; i++) {
            Path path = new Path();
            String s = "my text" + String.valueOf(i);
            mPaint.getTextPath(s, 0, s.length(), mMainCirclRadius * 2 / 3, mMainCircleCentr - 4, path);
            path.close(); // not required on 2.2/2.3 devices
            mTextPaths.add(path);
        }
    }
    if (mLastMillis == 0L) {
        mLastMillis = System.currentTimeMillis();
    }

    drawUpperCircle(canvas);
    drawBottomCircle(canvas);
    drawMainCircle(canvas);

    invalidate();

    if (((int) mRotation) % 10 == 0) {
        long millis = System.currentTimeMillis();
        Log.w("AnimateCanvas", "OnDraw called with mRotation == " + mRotation);
        Log.w("AnimateCanvas", "Last 10 degrees took millis: " + (millis - mLastMillis));
        mLastMillis = millis;
    }
}

private void drawUpperCircle(Canvas canvas) {
    canvas.save();
    canvas.rotate(mRotation, 0, mUpperCircleCentr);
    float rot = mRotation;
    mPaint.setColor(Color.CYAN);
    canvas.drawCircle(0, mUpperCircleCentr, mUpperCirclRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, 0, mUpperCircleCentr);
        rot += SEG_IN_GRAD;
        if (rot % 360 > 90 && rot % 360 < 270)
            continue;
        canvas.drawLine(0, mUpperCircleCentr, mUpperCirclRadius, mUpperCircleCentr, mPaint);
    }
    canvas.restore();
}

private void drawBottomCircle(Canvas canvas) {
    canvas.save();
    canvas.rotate(mRotation, 0, mBottomCircleCentr);
    float rot = mRotation;
    mPaint.setColor(Color.RED);
    canvas.drawCircle(0, mBottomCircleCentr, mBottomCirclRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, 0, mBottomCircleCentr);
        rot += SEG_IN_GRAD;
        if (rot % 360 > 90 && rot % 360 < 270)
            continue;
        canvas.drawLine(0, mBottomCircleCentr, mBottomCirclRadius, mBottomCircleCentr, mPaint);
    }
    canvas.restore();
}

private void drawMainCircle(Canvas canvas) {
    canvas.save();

    canvas.rotate(mRotation, 0, mMainCircleCentr);
    float rot = mRotation;
    mPaint.setColor(Color.argb(100, 100, 100, 100));
    canvas.drawCircle(0, mMainCircleCentr, mMainCirclRadius, mPaint);
    mPaint.setColor(Color.BLACK);
    for (int i = 0; i < SEG_COUNT; i++) {
        canvas.rotate(SEG_IN_GRAD, 0, mMainCircleCentr);
        rot += SEG_IN_GRAD;
        if (rot % 360 > 90 && rot % 360 < 270)
            continue;
        canvas.drawLine(0, mMainCircleCentr, mMainCirclRadius, mMainCircleCentr, mPaint);
        canvas.drawPath(mTextPaths.get(i), mPaint);
        // canvas.drawText("my text" + String.valueOf(i), mMainCirclRadius * 2 / 3, mMainCircleCentr - 4, mPaint);
    }
    canvas.restore();
}
}


回答2:

Your code is pretty nice and simple. You can optimize it by using less loops for instance, drawing things all together or combining variables, but this would quickly get messy.

I would recommend you to keep your drawing code more or less equal. You actually don't do the worst thing : instanciating objects, and it's clear and easy to maintain.

But you could maybe try to use a double buffer : drawing in a buffer in ram and flipping the buffer one shot on the screen. This generally performs quite well to get a constant animation pace. Use locking and unlocking of your canvas : Double buffering in Java on Android with canvas and surfaceview