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, letheavy rendering
take as much time as it needs to draw the picturefinal bitmap
- use it to store the final picture and display to the user if the app is calling onDraw function
Steps:
- After processing is triggered, the call is buffered and only the last available processed
- The rendering is performed into the
intermediate bitmap
(this is a long process) - After rendering is finished,
intermediate bitmap
is transferred to thefinal bitmap
(this should be quick) final bitmap
is drawn on the screen anytime app wants to refresh the screen (this should also be quick). Steps 3 and 4 shouldsynchronized
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 drawBitmap
s to the thread. As long as I removed drawColor
or turned off hardware acceleration, flickering disappeared.