1

Questions

  • What are the best practices for heavy rendering in Android views?
  • Is the suggested approach is pointing in the right direction?
  • What are the alternatives?

Description

My Android app scrolls through heavy rendered content. Rendering of one screen can take up to 500 ms. The final image should be displayed to the user.

Heavy rendering can be triggered multiple times per second (up to 20 times), simply via finger dragging.

In general only the last call matters. The intermediate non-processed calls can be dropped. If the intermediate call was processed though, it makes sense to display result to the user while the next/last call is rendering.

Obviously if you place heavy rendering into the main thread, the UI will be frozen and user experience will suffer.

Current implementation

Idea

Heavy rendering together with "last call matters" requirement let you think about using a separate thread and RxJava Flowable.

The idea is to have two bitmaps:

  • intermediate bitmap - use it in a thread, let heavy rendering take as much time as it needs to draw the picture
  • final bitmap - use it to store the final picture and display to the user if the app is calling onDraw function

Steps:

  1. After processing is triggered, the call is buffered and only the last available processed
  2. The rendering is performed into the intermediate bitmap (this is a long process)
  3. After rendering is finished, intermediate bitmap is transferred to the final bitmap (this should be quick)
  4. final bitmap is drawn on the screen anytime app wants to refresh the screen (this should also be quick). Steps 3 and 4 should synchronized to prevent flickering.

Schema

Schematic it looks like that:

   intermediate   -->   final   -->   screen
      bitmap            bitmap        bitmap 

      [long]           [quick]       [quick]

(Rendering thread)      ?   (--- Main thread ---)
|---------- sync1? ----------|
                       |------- sync2 ------|

Code

Technically it can be done like that (also schematic, some details are omitted):

// trigger initialization
void init() {
    mCanvasIntermediate = new Canvas(mBitmapIntermediate);
    mCanvasFinal = new Canvas(mBitmapFinal);

    rendering = PublishSubject.create();
    rendering
        .toFlowable(BackpressureStrategy.LATEST)
        .onBackpressureLatest()
        .observeOn(Schedulers.computation(), false, 1)
        .flatMapSingle(canvas -> Single.create(emitter -> {
            canvas.drawBitmap(...);
            emitter.onSuccess(result);
        }))
        .observeOn(AndroidSchedulers.mainThread()) // <-- comment to put invalidate to the thread
        .subscribe(result -> {
            mCanvasIntermediate.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            mCanvasFinal.drawBitmap(mBitmapIntermediate, ...);
            invalidate();
        });
}

// triggering
void triggerRendering() {
    rendering.onNext(mCanvasIntermediate);
}

// view ondraw
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(mBitmapFinal, ...);
}

Problem

In general it works, but not perfect:

  • if both rendering and output intermediate->final are placed to the main thread, the screen is freezing on rendering,
  • if only output intermediate->final is placed to the main thread, the screen is flickering,
  • if both rendering and output intermediate->final are placed to the calculation thread only a part of the screen is displayed during the rendering process.

Update:

This comment helped me a lot: https://stackoverflow.com/a/57610119/10475643

It appeared, that this line caused the whole skirmish:

mCanvasIntermediate.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

Because PorterDuff.Mode.CLEAR does not work well with hardware acceleration. This caused flickering and partial screen output. At the end I placed the both drawBitmaps to the thread. As long as I removed drawColor or turned off hardware acceleration, flickering disappeared.

Filip
  • 19,269
  • 7
  • 51
  • 60
mspnr
  • 416
  • 4
  • 12

0 Answers0