AnimatedImageDrawable: Eliminate unnecessary calls to redraw
Bug: 78866720 Test: Manual + systrace; existing CTS Previously, we set hasAnimations to true when the AnimatedImageDrawable, so that we would get a call to redraw. But if the image does not need to show its next frame yet, the redraw was unnecessary. Instead, add a new field to TreeInfo::Out, representing the delay time until the image will need to be redrawn - i.e. when the duration of the current frame has passed. Each call to prepareTree will post at most one message to redraw, in time for the earliest animated image to be redrawn. Post the message for one rendered frame ahead of time, so that when it is time to show the next frame, the image has already gotten the message to update. On a screen with a single animated image, this drops the number of calls to dispatchFrameCallbacks to as infrequent as possible. It is called only when we need to draw a new frame of the image. On a screen with multiple animated images, the calls may be redundant, but they will not be more frequent than they would be without this change. Switch to nsecs_t and systemTime internally, matching the rest of HWUI. Remove mDidDraw and related. Its purpose was to prevent advancing the animation while the image is not being drawn. But it isn't really necessary. If it's not drawn, onDraw is not called, which is where we trigger decoding. And onDraw already has a defense against getting too far ahead - if its timer indicates that it should skip a frame or show it very briefly, it will back up its timer. More importantly, mDidDraw caused a bug, when combined with less frequent redraws. If the display list containing the drawable doesn't need to be redrawn for other reasons, the drawable's timer never advanced, so its animation stopped. Fix software drawing. Compute the milliseconds in the future to draw the next frame, and add that to SystemClock.uptimeMillis() to compute the time to pass to scheduleSelf. Change-Id: I13aab49922fa300f73b327be25561d7120c09ec4
This commit is contained in:
@@ -239,11 +239,6 @@ static jlong AnimatedImageDrawable_nNativeByteSize(JNIEnv* env, jobject /*clazz*
|
||||
return drawable->byteSize();
|
||||
}
|
||||
|
||||
static void AnimatedImageDrawable_nMarkInvisible(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) {
|
||||
auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr);
|
||||
drawable->markInvisible();
|
||||
}
|
||||
|
||||
static void AnimatedImageDrawable_nSetMirrored(JNIEnv* env, jobject /*clazz*/, jlong nativePtr,
|
||||
jboolean mirrored) {
|
||||
auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr);
|
||||
@@ -264,7 +259,6 @@ static const JNINativeMethod gAnimatedImageDrawableMethods[] = {
|
||||
{ "nSetRepeatCount", "(JI)V", (void*) AnimatedImageDrawable_nSetRepeatCount },
|
||||
{ "nSetOnAnimationEndListener", "(JLandroid/graphics/drawable/AnimatedImageDrawable;)V", (void*) AnimatedImageDrawable_nSetOnAnimationEndListener },
|
||||
{ "nNativeByteSize", "(J)J", (void*) AnimatedImageDrawable_nNativeByteSize },
|
||||
{ "nMarkInvisible", "(J)V", (void*) AnimatedImageDrawable_nMarkInvisible },
|
||||
{ "nSetMirrored", "(JZ)V", (void*) AnimatedImageDrawable_nSetMirrored },
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
@@ -348,7 +349,7 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 {
|
||||
if (mRunnable == null) {
|
||||
mRunnable = this::invalidateSelf;
|
||||
}
|
||||
scheduleSelf(mRunnable, nextUpdate);
|
||||
scheduleSelf(mRunnable, nextUpdate + SystemClock.uptimeMillis());
|
||||
} else if (nextUpdate == FINISHED) {
|
||||
// This means the animation was drawn in software mode and ended.
|
||||
postOnAnimationEnd();
|
||||
@@ -430,23 +431,6 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 {
|
||||
return mState.mAutoMirrored;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
if (!super.setVisible(visible, restart)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mState.mNativePtr == 0) {
|
||||
throw new IllegalStateException("called setVisible on empty AnimatedImageDrawable");
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
nMarkInvisible(mState.mNativePtr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Animatable overrides
|
||||
/**
|
||||
* Return whether the animation is currently running.
|
||||
@@ -616,7 +600,5 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 {
|
||||
@FastNative
|
||||
private static native long nNativeByteSize(long nativePtr);
|
||||
@FastNative
|
||||
private static native void nMarkInvisible(long nativePtr);
|
||||
@FastNative
|
||||
private static native void nSetMirrored(long nativePtr, boolean mirror);
|
||||
}
|
||||
|
||||
@@ -108,6 +108,12 @@ public:
|
||||
// *OR* will post itself for the next vsync automatically, use this
|
||||
// only to avoid calling draw()
|
||||
bool canDrawThisFrame = true;
|
||||
// Sentinel for animatedImageDelay meaning there is no need to post such
|
||||
// a message.
|
||||
static constexpr nsecs_t kNoAnimatedImageDelay = -1;
|
||||
// This is used to post a message to redraw when it is time to draw the
|
||||
// next frame of an AnimatedImageDrawable.
|
||||
nsecs_t animatedImageDelay = kNoAnimatedImageDelay;
|
||||
} out;
|
||||
|
||||
// This flag helps to disable projection for receiver nodes that do not have any backward
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
#include <SkPicture.h>
|
||||
#include <SkRefCnt.h>
|
||||
#include <SkTLazy.h>
|
||||
#include <SkTime.h>
|
||||
|
||||
namespace android {
|
||||
|
||||
AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed)
|
||||
: mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed) {
|
||||
mTimeToShowNextSnapshot = mSkAnimatedImage->currentFrameDuration();
|
||||
mTimeToShowNextSnapshot = ms2ns(mSkAnimatedImage->currentFrameDuration());
|
||||
}
|
||||
|
||||
void AnimatedImageDrawable::syncProperties() {
|
||||
@@ -62,28 +61,42 @@ bool AnimatedImageDrawable::nextSnapshotReady() const {
|
||||
}
|
||||
|
||||
// Only called on the RenderThread while UI thread is locked.
|
||||
bool AnimatedImageDrawable::isDirty() {
|
||||
const double currentTime = SkTime::GetMSecs();
|
||||
const double lastWallTime = mLastWallTime;
|
||||
bool AnimatedImageDrawable::isDirty(nsecs_t* outDelay) {
|
||||
*outDelay = 0;
|
||||
const nsecs_t currentTime = systemTime(CLOCK_MONOTONIC);
|
||||
const nsecs_t lastWallTime = mLastWallTime;
|
||||
|
||||
mLastWallTime = currentTime;
|
||||
if (!mRunning) {
|
||||
mDidDraw = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_lock lock{mSwapLock};
|
||||
if (mDidDraw) {
|
||||
mCurrentTime += currentTime - lastWallTime;
|
||||
mDidDraw = false;
|
||||
}
|
||||
mCurrentTime += currentTime - lastWallTime;
|
||||
|
||||
if (!mNextSnapshot.valid()) {
|
||||
// Need to trigger onDraw in order to start decoding the next frame.
|
||||
*outDelay = mTimeToShowNextSnapshot - mCurrentTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
return nextSnapshotReady() && mCurrentTime >= mTimeToShowNextSnapshot;
|
||||
if (mTimeToShowNextSnapshot > mCurrentTime) {
|
||||
*outDelay = mTimeToShowNextSnapshot - mCurrentTime;
|
||||
} else if (nextSnapshotReady()) {
|
||||
// We have not yet updated mTimeToShowNextSnapshot. Read frame duration
|
||||
// directly from mSkAnimatedImage.
|
||||
lock.unlock();
|
||||
std::unique_lock imageLock{mImageLock};
|
||||
*outDelay = ms2ns(mSkAnimatedImage->currentFrameDuration());
|
||||
return true;
|
||||
} else {
|
||||
// The next snapshot has not yet been decoded, but we've already passed
|
||||
// time to draw it. There's not a good way to know when decoding will
|
||||
// finish, so request an update immediately.
|
||||
*outDelay = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only called on the AnimatedImageThread.
|
||||
@@ -91,7 +104,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::decodeNextFrame() {
|
||||
Snapshot snap;
|
||||
{
|
||||
std::unique_lock lock{mImageLock};
|
||||
snap.mDuration = mSkAnimatedImage->decodeNextFrame();
|
||||
snap.mDurationMS = mSkAnimatedImage->decodeNextFrame();
|
||||
snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot());
|
||||
}
|
||||
|
||||
@@ -105,7 +118,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::reset() {
|
||||
std::unique_lock lock{mImageLock};
|
||||
mSkAnimatedImage->reset();
|
||||
snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot());
|
||||
snap.mDuration = mSkAnimatedImage->currentFrameDuration();
|
||||
snap.mDurationMS = mSkAnimatedImage->currentFrameDuration();
|
||||
}
|
||||
|
||||
return snap;
|
||||
@@ -127,8 +140,6 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
|
||||
canvas->scale(-1, 1);
|
||||
}
|
||||
|
||||
mDidDraw = true;
|
||||
|
||||
const bool starting = mStarting;
|
||||
mStarting = false;
|
||||
|
||||
@@ -157,12 +168,12 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
|
||||
std::unique_lock lock{mSwapLock};
|
||||
if (mCurrentTime >= mTimeToShowNextSnapshot) {
|
||||
mSnapshot = mNextSnapshot.get();
|
||||
const double timeToShowCurrentSnap = mTimeToShowNextSnapshot;
|
||||
if (mSnapshot.mDuration == SkAnimatedImage::kFinished) {
|
||||
const nsecs_t timeToShowCurrentSnap = mTimeToShowNextSnapshot;
|
||||
if (mSnapshot.mDurationMS == SkAnimatedImage::kFinished) {
|
||||
finalFrame = true;
|
||||
mRunning = false;
|
||||
} else {
|
||||
mTimeToShowNextSnapshot += mSnapshot.mDuration;
|
||||
mTimeToShowNextSnapshot += ms2ns(mSnapshot.mDurationMS);
|
||||
if (mCurrentTime >= mTimeToShowNextSnapshot) {
|
||||
// This would mean showing the current frame very briefly. It's
|
||||
// possible that not being displayed for a time resulted in
|
||||
@@ -192,7 +203,7 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
|
||||
}
|
||||
}
|
||||
|
||||
double AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
|
||||
int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
|
||||
SkAutoCanvasRestore acr(canvas, false);
|
||||
if (mStagingProperties.mAlpha != SK_AlphaOPAQUE || mStagingProperties.mColorFilter.get()) {
|
||||
SkPaint paint;
|
||||
@@ -211,69 +222,69 @@ double AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
|
||||
// to redraw.
|
||||
std::unique_lock lock{mImageLock};
|
||||
canvas->drawDrawable(mSkAnimatedImage.get());
|
||||
return 0.0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (mStarting) {
|
||||
mStarting = false;
|
||||
double duration = 0.0;
|
||||
int durationMS = 0;
|
||||
{
|
||||
std::unique_lock lock{mImageLock};
|
||||
mSkAnimatedImage->reset();
|
||||
duration = mSkAnimatedImage->currentFrameDuration();
|
||||
durationMS = mSkAnimatedImage->currentFrameDuration();
|
||||
}
|
||||
{
|
||||
std::unique_lock lock{mSwapLock};
|
||||
mLastWallTime = 0.0;
|
||||
mTimeToShowNextSnapshot = duration;
|
||||
mLastWallTime = 0;
|
||||
// The current time will be added later, below.
|
||||
mTimeToShowNextSnapshot = ms2ns(durationMS);
|
||||
}
|
||||
}
|
||||
|
||||
bool update = false;
|
||||
{
|
||||
const double currentTime = SkTime::GetMSecs();
|
||||
const nsecs_t currentTime = systemTime(CLOCK_MONOTONIC);
|
||||
std::unique_lock lock{mSwapLock};
|
||||
// mLastWallTime starts off at 0. If it is still 0, just update it to
|
||||
// the current time and avoid updating
|
||||
if (mLastWallTime == 0.0) {
|
||||
if (mLastWallTime == 0) {
|
||||
mCurrentTime = currentTime;
|
||||
// mTimeToShowNextSnapshot is already set to the duration of the
|
||||
// first frame.
|
||||
mTimeToShowNextSnapshot += currentTime;
|
||||
} else if (mRunning && mDidDraw) {
|
||||
} else if (mRunning) {
|
||||
mCurrentTime += currentTime - mLastWallTime;
|
||||
update = mCurrentTime >= mTimeToShowNextSnapshot;
|
||||
}
|
||||
mLastWallTime = currentTime;
|
||||
}
|
||||
|
||||
double duration = 0.0;
|
||||
int durationMS = 0;
|
||||
{
|
||||
std::unique_lock lock{mImageLock};
|
||||
if (update) {
|
||||
duration = mSkAnimatedImage->decodeNextFrame();
|
||||
durationMS = mSkAnimatedImage->decodeNextFrame();
|
||||
}
|
||||
|
||||
canvas->drawDrawable(mSkAnimatedImage.get());
|
||||
}
|
||||
|
||||
mDidDraw = true;
|
||||
|
||||
std::unique_lock lock{mSwapLock};
|
||||
if (update) {
|
||||
if (duration == SkAnimatedImage::kFinished) {
|
||||
if (durationMS == SkAnimatedImage::kFinished) {
|
||||
mRunning = false;
|
||||
return duration;
|
||||
return SkAnimatedImage::kFinished;
|
||||
}
|
||||
|
||||
const double timeToShowCurrentSnapshot = mTimeToShowNextSnapshot;
|
||||
mTimeToShowNextSnapshot += duration;
|
||||
const nsecs_t timeToShowCurrentSnapshot = mTimeToShowNextSnapshot;
|
||||
mTimeToShowNextSnapshot += ms2ns(durationMS);
|
||||
if (mCurrentTime >= mTimeToShowNextSnapshot) {
|
||||
// As in onDraw, prevent speedy catch-up behavior.
|
||||
mCurrentTime = timeToShowCurrentSnapshot;
|
||||
}
|
||||
}
|
||||
return mTimeToShowNextSnapshot;
|
||||
|
||||
return ns2ms(mTimeToShowNextSnapshot - mCurrentTime);
|
||||
}
|
||||
|
||||
} // namespace android
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <cutils/compiler.h>
|
||||
#include <utils/Macros.h>
|
||||
#include <utils/RefBase.h>
|
||||
#include <utils/Timers.h>
|
||||
|
||||
#include <SkAnimatedImage.h>
|
||||
#include <SkCanvas.h>
|
||||
@@ -50,12 +51,15 @@ public:
|
||||
AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed);
|
||||
|
||||
/**
|
||||
* This updates the internal time and returns true if the animation needs
|
||||
* to be redrawn.
|
||||
* This updates the internal time and returns true if the image needs
|
||||
* to be redrawn this frame.
|
||||
*
|
||||
* This is called on RenderThread, while the UI thread is locked.
|
||||
*
|
||||
* @param outDelay Nanoseconds in the future when the following frame
|
||||
* will need to be drawn. 0 if not running.
|
||||
*/
|
||||
bool isDirty();
|
||||
bool isDirty(nsecs_t* outDelay);
|
||||
|
||||
int getStagingAlpha() const { return mStagingProperties.mAlpha; }
|
||||
void setStagingAlpha(int alpha) { mStagingProperties.mAlpha = alpha; }
|
||||
@@ -68,7 +72,9 @@ public:
|
||||
virtual SkRect onGetBounds() override { return mSkAnimatedImage->getBounds(); }
|
||||
|
||||
// Draw to software canvas, and return time to next draw.
|
||||
double drawStaging(SkCanvas* canvas);
|
||||
// 0 means the animation is not running.
|
||||
// -1 means the animation advanced to the final frame.
|
||||
int drawStaging(SkCanvas* canvas);
|
||||
|
||||
// Returns true if the animation was started; false otherwise (e.g. it was
|
||||
// already running)
|
||||
@@ -84,11 +90,9 @@ public:
|
||||
mEndListener = std::move(listener);
|
||||
}
|
||||
|
||||
void markInvisible() { mDidDraw = false; }
|
||||
|
||||
struct Snapshot {
|
||||
sk_sp<SkPicture> mPic;
|
||||
int mDuration;
|
||||
int mDurationMS;
|
||||
|
||||
Snapshot() = default;
|
||||
|
||||
@@ -124,16 +128,13 @@ private:
|
||||
bool nextSnapshotReady() const;
|
||||
|
||||
// When to switch from mSnapshot to mNextSnapshot.
|
||||
double mTimeToShowNextSnapshot = 0.0;
|
||||
nsecs_t mTimeToShowNextSnapshot = 0;
|
||||
|
||||
// The current time for the drawable itself.
|
||||
double mCurrentTime = 0.0;
|
||||
nsecs_t mCurrentTime = 0;
|
||||
|
||||
// The wall clock of the last time we called isDirty.
|
||||
double mLastWallTime = 0.0;
|
||||
|
||||
// Whether we drew since the last call to isDirty.
|
||||
bool mDidDraw = false;
|
||||
nsecs_t mLastWallTime = 0;
|
||||
|
||||
// Locked when assigning snapshots and times. Operations while this is held
|
||||
// should be short.
|
||||
|
||||
@@ -93,12 +93,18 @@ bool SkiaDisplayList::prepareListAndChildren(
|
||||
|
||||
bool isDirty = false;
|
||||
for (auto& animatedImage : mAnimatedImages) {
|
||||
nsecs_t timeTilNextFrame = TreeInfo::Out::kNoAnimatedImageDelay;
|
||||
// If any animated image in the display list needs updated, then damage the node.
|
||||
if (animatedImage->isDirty()) {
|
||||
if (animatedImage->isDirty(&timeTilNextFrame)) {
|
||||
isDirty = true;
|
||||
}
|
||||
if (animatedImage->isRunning()) {
|
||||
info.out.hasAnimations = true;
|
||||
|
||||
if (animatedImage->isRunning() &&
|
||||
timeTilNextFrame != TreeInfo::Out::kNoAnimatedImageDelay) {
|
||||
auto& delay = info.out.animatedImageDelay;
|
||||
if (delay == TreeInfo::Out::kNoAnimatedImageDelay || timeTilNextFrame < delay) {
|
||||
delay = timeTilNextFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode*
|
||||
IContextFactory* contextFactory,
|
||||
std::unique_ptr<IRenderPipeline> renderPipeline)
|
||||
: mRenderThread(thread)
|
||||
, mGenerationID(0)
|
||||
, mOpaque(!translucent)
|
||||
, mAnimationContext(contextFactory->createAnimationContext(mRenderThread.timeLord()))
|
||||
, mJankTracker(&thread.globalProfileData(), thread.mainDisplayInfo())
|
||||
@@ -196,6 +197,7 @@ void CanvasContext::setSurface(sp<Surface>&& surface) {
|
||||
mSwapHistory.clear();
|
||||
} else {
|
||||
mRenderThread.removeFrameCallback(this);
|
||||
mGenerationID++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +206,7 @@ void CanvasContext::setSwapBehavior(SwapBehavior swapBehavior) {
|
||||
}
|
||||
|
||||
bool CanvasContext::pauseSurface() {
|
||||
mGenerationID++;
|
||||
return mRenderThread.removeFrameCallback(this);
|
||||
}
|
||||
|
||||
@@ -211,6 +214,7 @@ void CanvasContext::setStopped(bool stopped) {
|
||||
if (mStopped != stopped) {
|
||||
mStopped = stopped;
|
||||
if (mStopped) {
|
||||
mGenerationID++;
|
||||
mRenderThread.removeFrameCallback(this);
|
||||
mRenderPipeline->onStop();
|
||||
} else if (mIsDirty && hasSurface()) {
|
||||
@@ -383,6 +387,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
|
||||
mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
|
||||
}
|
||||
|
||||
bool postedFrameCallback = false;
|
||||
if (info.out.hasAnimations || !info.out.canDrawThisFrame) {
|
||||
if (CC_UNLIKELY(!Properties::enableRTAnimations)) {
|
||||
info.out.requiresUiRedraw = true;
|
||||
@@ -391,6 +396,24 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
|
||||
// If animationsNeedsRedraw is set don't bother posting for an RT anim
|
||||
// as we will just end up fighting the UI thread.
|
||||
mRenderThread.postFrameCallback(this);
|
||||
postedFrameCallback = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!postedFrameCallback &&
|
||||
info.out.animatedImageDelay != TreeInfo::Out::kNoAnimatedImageDelay) {
|
||||
// Subtract the time of one frame so it can be displayed on time.
|
||||
const nsecs_t kFrameTime = mRenderThread.timeLord().frameIntervalNanos();
|
||||
if (info.out.animatedImageDelay <= kFrameTime) {
|
||||
mRenderThread.postFrameCallback(this);
|
||||
} else {
|
||||
const auto delay = info.out.animatedImageDelay - kFrameTime;
|
||||
int genId = mGenerationID;
|
||||
mRenderThread.queue().postDelayed(delay, [this, genId]() {
|
||||
if (mGenerationID == genId) {
|
||||
mRenderThread.postFrameCallback(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,6 +421,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
|
||||
void CanvasContext::stopDrawing() {
|
||||
mRenderThread.removeFrameCallback(this);
|
||||
mAnimationContext->pauseAnimators();
|
||||
mGenerationID++;
|
||||
}
|
||||
|
||||
void CanvasContext::notifyFramePending() {
|
||||
|
||||
@@ -213,6 +213,9 @@ private:
|
||||
// stopped indicates the CanvasContext will reject actual redraw operations,
|
||||
// and defer repaint until it is un-stopped
|
||||
bool mStopped = false;
|
||||
// Incremented each time the CanvasContext is stopped. Used to ignore
|
||||
// delayed messages that are triggered after stopping.
|
||||
int mGenerationID;
|
||||
// CanvasContext is dirty if it has received an update that it has not
|
||||
// painted onto its surface.
|
||||
bool mIsDirty = false;
|
||||
|
||||
Reference in New Issue
Block a user