Battery consumption with custom animated drawable

2019-08-30 13:49发布

问题:

Loooong time viewer, finally getting round to signing up here at StackOverflow!

After a very long time searching for a way to do a scrolling background of a ViewGroup in Android, I've developed the following:

public class SlidingDrawable extends Drawable implements Drawable.Callback {
private static final String TAG = "SlidingDraw";
private static float STEP_SIZE = 1.0f;

private BitmapDrawable mBitmap;
private Context mContext;

private float mPosX;
private int mBitmapWidth;

private Runnable mInvalidater;
private Handler mHandler;

public SlidingDrawable(Context c){
    mContext = c;

    // use this as the callback as we're implementing the interface
    setCallback(this);

    mHandler = new Handler();
    mInvalidater = new Runnable(){
        @Override
        public void run(){
            // decrement the drawables step size
            mPosX -= SlidingDrawable.STEP_SIZE;

            /*
             * Check to see if the current position is at point where it should 
             * loop.  If so, reset back to 0 to restart
             */
            if(Math.abs(mPosX) >= mBitmapWidth) mPosX = 0;
            // redraw
            invalidateDrawable(null);
        }
    };
}
public static void setStepSize(float newSize){
    SlidingDrawable.STEP_SIZE = newSize;
}
public void createBitmap(String path, ViewGroup parent){
    // height of the parent container
    int height = parent.getHeight();

    /* Initialize local variables
     *  bgBitmap    - the resulting bitmap to send into SlidingDrawable instance
     *  imageStream - raw bitmap data to be decoded into bgBitmap
     */     
    WindowManager wMgr = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    int mScreenWidth = wMgr.getDefaultDisplay().getWidth();

    InputStream imageStream;
    Matrix imgMatrix = new Matrix();
    Bitmap bitmap = null;
    try {
        imageStream = mContext.getAssets().open(path);

        // create a temporary bitmap object for basic data
        Bitmap temp = BitmapFactory.decodeStream(imageStream);
        int width = temp.getWidth();

        // find the width difference as a percentage to apply to the 
        // transformation matrix
        float widthDifference = ((float)mScreenWidth) / (float)(width / 2);
        imgMatrix.postScale(widthDifference, 0, 0f, 0f);

        // create a copy of the bitmap, scaled correctly to maintain loop
        bitmap = Bitmap.createScaledBitmap(temp, (int)(width * widthDifference), height, true);

        // recycle the temp bitmap
        temp.recycle();
    } catch (IOException e) {
        e.printStackTrace();
    }   
    mBitmap = new BitmapDrawable(bitmap);

    // required
    mBitmapWidth = getIntrinsicWidth() / 2;
    Rect bounds = new Rect(0, 0, getIntrinsicWidth(), getIntrinsicHeight());        
    setBounds(bounds);
}
@Override
public void draw(Canvas canvas) {
    canvas.drawBitmap(mBitmap.getBitmap(), mPosX, 0f, null);
    scheduleDrawable(this, mInvalidater, SystemClock.uptimeMillis());
}
@Override
public int getOpacity() {
    return PixelFormat.OPAQUE;
}

@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
    mHandler.postAtTime(what, who, when);
}

@Override
public void unscheduleDrawable(Drawable who, Runnable what) {
    mHandler.removeCallbacks(what, who);
}
@Override
public void invalidateDrawable(Drawable who) {
    invalidateSelf();
}
/* 
 * Methods not directly used or called omitted
 * 
 */

}

It is used in the Activity like so:

@Override
public void onWindowFocusChanged(boolean focus){
    // set the background of the root view of main.xml
    SlidingDrawable drawable = new SlidingDrawable(getApplicationContext());
    drawable.createBitmap("bgimg/basebg.jpg", mRoot);
    mRoot.setBackgroundDrawable(drawable);
}

Long story short, the basebg.jpg image is a tileable image roughly 1600x480. The constructor for SlidingDrawable scales and moves and yaddah yaddah. It works.

Now, the problem is, it seems really inefficient to do it like this. I can't seem to find much information on this sort of implementation, so I'm in the dark on where I can cut CPU cycles, or even if I'm using the method calls correctly.

My questions include:

  • Is it better to drawBitmap as opposed to using setTranslate() or postTranslate and draw the bitmap using a Matrix?
  • Is it better to use drawBitmap, or the canvas functions such as translate(), save(), and restore()?
  • What rate does the draw() method get called at, and is there a way to limit it to, say, 24 FPS o limit redraws?
  • What the heck is the "when" parameter of these sorts of things? Passing in SystemClock.uptimeMillis() is the only one that worked, and trying to delay it by adding a " + 100" or something to fire every 100ms just made it stutter.

I've researched this as much as I can... I'm leaving it to StackOverflow now :)

回答1:

After some time with the drawing board, I simplified the functions down. Essentially, it was sending an invalidate() call on every SystemClock.uptimeMillis(), doing one redraw for each step change.

So, I removed the Drawable.Callback interface and passed the invalidateSelf() call directly from the Handler, removing the intermediary interface methods, which didn't seem to do anything special anyway.

There was a slight difference in the CPU usage using drawBitmap(Bitmap source, int X, int Y, Paint p) vs. drawBitmap(Bitmap source, Matrix matrix, Paint p), so I opted for the latter to save cycles.

New methods are as follows:

// Runnable
mInvalidater = new Runnable(){
    @Override
    public void run(){
        // decrement the drawables step size
        mPosX -= SlidingDrawable.STEP_SIZE;
        if(Math.abs(mPosX) >= mBitmapWidth){
            mPosX = 0;
            mImageMatrix.setTranslate(0, 0);
        }
        mImageMatrix.postTranslate(-STEP_SIZE, 0);
        SlidingDrawable.this.invalidateSelf();
    }
};
// Draw method
@Override
public void draw(Canvas canvas) {
    canvas.drawBitmap(mBitmap.getBitmap(), mImageMatrix, null);
    mHandler.postAtTime(mInvalidater, SystemClock.uptimeMillis() + 64); 
}

I tested the battery results by unplugging the phone, running the application for around a minute, then opening the battery usage on my Moto Droid. Before, the battery usage surpassed the Display, now it sits comfortably below.

Angry Birds was also a benchmark, by running the opening screen (where the bg scrolls and the birds fly everywhere) for the same amount of time, in some cases my app sat below Angry Birds, but not always.

CPU usage was checked using the ADB shell command dumpsys cpuinfo as there seems to be a problem viewing CPU info on through the DDMS on devices running 2.2.

I'd still be up to hear other thoughts on this, but for now, it's solved.