Bug: 128805564
Test: Manual + systrace; hwui_unit_tests; CtsUiRenderingTestCases
Only update a VectorDrawable's cache if it is onscreen. This fixes a
Twitter use case where the app has a ProgressBar that is exactly one
pixel offscreen. Prior to this CL, we repeatedly drew the ProgressBar's
AVD to a GPU surface, even though we clip it out later and never draw
that GPU surface. Now, we recognize that the AVD is outside of the
bounds of the screen, so we never draw to the GPU surface.
TreeInfo:
- store the size of the screen, retrieved from
CanvasContext::getNextFrameSize.
SkiaDisplayList:
- Store the matrix at the time of recording a VectorDrawable. Concat
that with the current matrix to determine whether the VD is on screen,
based on the TreeInfo. If it is offscreen, do not add it to the list
of AVDs that need to be updated ahead of rendering.
- In addition, if it is offscreen (or not dirty), do not call
setPropertyChangeWillBeConsumed(true). This prevents triggering
dispatchFrameCallbacks to update on the RenderThread when there is no
need to. This also mimics what would happen if the View/RenderNode had
been completely offscreen.
- Add a method to append an AVD to mVectorDrawables. Now that the vector
is of Pairs, this simplifies the call sites. Add a second helper to
just add an AVD without a matrix, for use in tests.
SkiaRecordingCanvas:
- get the current matrix and store it in the display list along with the
AVD.
CanvasContext:
- add getNextFrameSize, for reporting the size of the next frame without
dequeuing it
VectorDrawable.cpp:
- call quickReject to potentially short circuit drawing. This is for a
hypothetical use case (verified in a test app) where the containing
RenderNode is partially onscreen, but the AVD itself is not. Even
without the change to VectorDrawable.cpp, we skip uploading to the GPU
cache, the SkiaDisplayList still attempts to draw it. This change
keeps us from drawing it at all.
SkiaDisplayListTests.cpp:
- Now that I've hidden mVectorDrawables, call the new public APIs.
- prepareListAndChildren test
- for the clean VD, assert that getPropertyChangeWillBeConsumed
returns FALSE. This is due to the behavior change that we do not
set it unless the VD is dirty.
- set the bounds, so our onscreen check works.
- Add another test for prepareListAndChildren, which puts VDs offscreen.
Change-Id: Iae0a07adcf58e7884e0854720de644e7b2faf2bf
711 lines
25 KiB
C++
711 lines
25 KiB
C++
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "VectorDrawable.h"
|
|
|
|
#include <utils/Log.h>
|
|
#include "PathParser.h"
|
|
#include "SkColorFilter.h"
|
|
#include "SkImageInfo.h"
|
|
#include "SkShader.h"
|
|
#include "utils/Macros.h"
|
|
#include "utils/TraceUtils.h"
|
|
#include "utils/VectorDrawableUtils.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
namespace android {
|
|
namespace uirenderer {
|
|
namespace VectorDrawable {
|
|
|
|
const int Tree::MAX_CACHED_BITMAP_SIZE = 2048;
|
|
|
|
void Path::dump() {
|
|
ALOGD("Path: %s has %zu points", mName.c_str(), mProperties.getData().points.size());
|
|
}
|
|
|
|
// Called from UI thread during the initial setup/theme change.
|
|
Path::Path(const char* pathStr, size_t strLength) {
|
|
PathParser::ParseResult result;
|
|
Data data;
|
|
PathParser::getPathDataFromAsciiString(&data, &result, pathStr, strLength);
|
|
mStagingProperties.setData(data);
|
|
}
|
|
|
|
Path::Path(const Path& path) : Node(path) {
|
|
mStagingProperties.syncProperties(path.mStagingProperties);
|
|
}
|
|
|
|
const SkPath& Path::getUpdatedPath(bool useStagingData, SkPath* tempStagingPath) {
|
|
if (useStagingData) {
|
|
tempStagingPath->reset();
|
|
VectorDrawableUtils::verbsToPath(tempStagingPath, mStagingProperties.getData());
|
|
return *tempStagingPath;
|
|
} else {
|
|
if (mSkPathDirty) {
|
|
mSkPath.reset();
|
|
VectorDrawableUtils::verbsToPath(&mSkPath, mProperties.getData());
|
|
mSkPathDirty = false;
|
|
}
|
|
return mSkPath;
|
|
}
|
|
}
|
|
|
|
void Path::syncProperties() {
|
|
if (mStagingPropertiesDirty) {
|
|
mProperties.syncProperties(mStagingProperties);
|
|
} else {
|
|
mStagingProperties.syncProperties(mProperties);
|
|
}
|
|
mStagingPropertiesDirty = false;
|
|
}
|
|
|
|
FullPath::FullPath(const FullPath& path) : Path(path) {
|
|
mStagingProperties.syncProperties(path.mStagingProperties);
|
|
}
|
|
|
|
static void applyTrim(SkPath* outPath, const SkPath& inPath, float trimPathStart, float trimPathEnd,
|
|
float trimPathOffset) {
|
|
if (trimPathStart == 0.0f && trimPathEnd == 1.0f) {
|
|
*outPath = inPath;
|
|
return;
|
|
}
|
|
outPath->reset();
|
|
if (trimPathStart == trimPathEnd) {
|
|
// Trimmed path should be empty.
|
|
return;
|
|
}
|
|
SkPathMeasure measure(inPath, false);
|
|
float len = SkScalarToFloat(measure.getLength());
|
|
float start = len * fmod((trimPathStart + trimPathOffset), 1.0f);
|
|
float end = len * fmod((trimPathEnd + trimPathOffset), 1.0f);
|
|
|
|
if (start > end) {
|
|
measure.getSegment(start, len, outPath, true);
|
|
if (end > 0) {
|
|
measure.getSegment(0, end, outPath, true);
|
|
}
|
|
} else {
|
|
measure.getSegment(start, end, outPath, true);
|
|
}
|
|
}
|
|
|
|
const SkPath& FullPath::getUpdatedPath(bool useStagingData, SkPath* tempStagingPath) {
|
|
if (!useStagingData && !mSkPathDirty && !mProperties.mTrimDirty) {
|
|
return mTrimmedSkPath;
|
|
}
|
|
Path::getUpdatedPath(useStagingData, tempStagingPath);
|
|
SkPath* outPath;
|
|
if (useStagingData) {
|
|
SkPath inPath = *tempStagingPath;
|
|
applyTrim(tempStagingPath, inPath, mStagingProperties.getTrimPathStart(),
|
|
mStagingProperties.getTrimPathEnd(), mStagingProperties.getTrimPathOffset());
|
|
outPath = tempStagingPath;
|
|
} else {
|
|
if (mProperties.getTrimPathStart() != 0.0f || mProperties.getTrimPathEnd() != 1.0f) {
|
|
mProperties.mTrimDirty = false;
|
|
applyTrim(&mTrimmedSkPath, mSkPath, mProperties.getTrimPathStart(),
|
|
mProperties.getTrimPathEnd(), mProperties.getTrimPathOffset());
|
|
outPath = &mTrimmedSkPath;
|
|
} else {
|
|
outPath = &mSkPath;
|
|
}
|
|
}
|
|
const FullPathProperties& properties = useStagingData ? mStagingProperties : mProperties;
|
|
bool setFillPath = properties.getFillGradient() != nullptr ||
|
|
properties.getFillColor() != SK_ColorTRANSPARENT;
|
|
if (setFillPath) {
|
|
SkPath::FillType ft = static_cast<SkPath::FillType>(properties.getFillType());
|
|
outPath->setFillType(ft);
|
|
}
|
|
return *outPath;
|
|
}
|
|
|
|
void FullPath::dump() {
|
|
Path::dump();
|
|
ALOGD("stroke width, color, alpha: %f, %d, %f, fill color, alpha: %d, %f",
|
|
mProperties.getStrokeWidth(), mProperties.getStrokeColor(), mProperties.getStrokeAlpha(),
|
|
mProperties.getFillColor(), mProperties.getFillAlpha());
|
|
}
|
|
|
|
inline SkColor applyAlpha(SkColor color, float alpha) {
|
|
int alphaBytes = SkColorGetA(color);
|
|
return SkColorSetA(color, alphaBytes * alpha);
|
|
}
|
|
|
|
void FullPath::draw(SkCanvas* outCanvas, bool useStagingData) {
|
|
const FullPathProperties& properties = useStagingData ? mStagingProperties : mProperties;
|
|
SkPath tempStagingPath;
|
|
const SkPath& renderPath = getUpdatedPath(useStagingData, &tempStagingPath);
|
|
|
|
// Draw path's fill, if fill color or gradient is valid
|
|
bool needsFill = false;
|
|
SkPaint paint;
|
|
if (properties.getFillGradient() != nullptr) {
|
|
paint.setColor(applyAlpha(SK_ColorBLACK, properties.getFillAlpha()));
|
|
paint.setShader(sk_sp<SkShader>(SkSafeRef(properties.getFillGradient())));
|
|
needsFill = true;
|
|
} else if (properties.getFillColor() != SK_ColorTRANSPARENT) {
|
|
paint.setColor(applyAlpha(properties.getFillColor(), properties.getFillAlpha()));
|
|
needsFill = true;
|
|
}
|
|
|
|
if (needsFill) {
|
|
paint.setStyle(SkPaint::Style::kFill_Style);
|
|
paint.setAntiAlias(mAntiAlias);
|
|
outCanvas->drawPath(renderPath, paint);
|
|
}
|
|
|
|
// Draw path's stroke, if stroke color or Gradient is valid
|
|
bool needsStroke = false;
|
|
if (properties.getStrokeGradient() != nullptr) {
|
|
paint.setColor(applyAlpha(SK_ColorBLACK, properties.getStrokeAlpha()));
|
|
paint.setShader(sk_sp<SkShader>(SkSafeRef(properties.getStrokeGradient())));
|
|
needsStroke = true;
|
|
} else if (properties.getStrokeColor() != SK_ColorTRANSPARENT) {
|
|
paint.setColor(applyAlpha(properties.getStrokeColor(), properties.getStrokeAlpha()));
|
|
needsStroke = true;
|
|
}
|
|
if (needsStroke) {
|
|
paint.setStyle(SkPaint::Style::kStroke_Style);
|
|
paint.setAntiAlias(mAntiAlias);
|
|
paint.setStrokeJoin(SkPaint::Join(properties.getStrokeLineJoin()));
|
|
paint.setStrokeCap(SkPaint::Cap(properties.getStrokeLineCap()));
|
|
paint.setStrokeMiter(properties.getStrokeMiterLimit());
|
|
paint.setStrokeWidth(properties.getStrokeWidth());
|
|
outCanvas->drawPath(renderPath, paint);
|
|
}
|
|
}
|
|
|
|
void FullPath::syncProperties() {
|
|
Path::syncProperties();
|
|
|
|
if (mStagingPropertiesDirty) {
|
|
mProperties.syncProperties(mStagingProperties);
|
|
} else {
|
|
// Update staging property with property values from animation.
|
|
mStagingProperties.syncProperties(mProperties);
|
|
}
|
|
mStagingPropertiesDirty = false;
|
|
}
|
|
|
|
REQUIRE_COMPATIBLE_LAYOUT(FullPath::FullPathProperties::PrimitiveFields);
|
|
|
|
static_assert(sizeof(float) == sizeof(int32_t), "float is not the same size as int32_t");
|
|
static_assert(sizeof(SkColor) == sizeof(int32_t), "SkColor is not the same size as int32_t");
|
|
|
|
bool FullPath::FullPathProperties::copyProperties(int8_t* outProperties, int length) const {
|
|
int propertyDataSize = sizeof(FullPathProperties::PrimitiveFields);
|
|
if (length != propertyDataSize) {
|
|
LOG_ALWAYS_FATAL("Properties needs exactly %d bytes, a byte array of size %d is provided",
|
|
propertyDataSize, length);
|
|
return false;
|
|
}
|
|
|
|
PrimitiveFields* out = reinterpret_cast<PrimitiveFields*>(outProperties);
|
|
*out = mPrimitiveFields;
|
|
return true;
|
|
}
|
|
|
|
void FullPath::FullPathProperties::setColorPropertyValue(int propertyId, int32_t value) {
|
|
Property currentProperty = static_cast<Property>(propertyId);
|
|
if (currentProperty == Property::strokeColor) {
|
|
setStrokeColor(value);
|
|
} else if (currentProperty == Property::fillColor) {
|
|
setFillColor(value);
|
|
} else {
|
|
LOG_ALWAYS_FATAL(
|
|
"Error setting color property on FullPath: No valid property"
|
|
" with id: %d",
|
|
propertyId);
|
|
}
|
|
}
|
|
|
|
void FullPath::FullPathProperties::setPropertyValue(int propertyId, float value) {
|
|
Property property = static_cast<Property>(propertyId);
|
|
switch (property) {
|
|
case Property::strokeWidth:
|
|
setStrokeWidth(value);
|
|
break;
|
|
case Property::strokeAlpha:
|
|
setStrokeAlpha(value);
|
|
break;
|
|
case Property::fillAlpha:
|
|
setFillAlpha(value);
|
|
break;
|
|
case Property::trimPathStart:
|
|
setTrimPathStart(value);
|
|
break;
|
|
case Property::trimPathEnd:
|
|
setTrimPathEnd(value);
|
|
break;
|
|
case Property::trimPathOffset:
|
|
setTrimPathOffset(value);
|
|
break;
|
|
default:
|
|
LOG_ALWAYS_FATAL("Invalid property id: %d for animation", propertyId);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ClipPath::draw(SkCanvas* outCanvas, bool useStagingData) {
|
|
SkPath tempStagingPath;
|
|
outCanvas->clipPath(getUpdatedPath(useStagingData, &tempStagingPath));
|
|
}
|
|
|
|
Group::Group(const Group& group) : Node(group) {
|
|
mStagingProperties.syncProperties(group.mStagingProperties);
|
|
}
|
|
|
|
void Group::draw(SkCanvas* outCanvas, bool useStagingData) {
|
|
// Save the current clip and matrix information, which is local to this group.
|
|
SkAutoCanvasRestore saver(outCanvas, true);
|
|
// apply the current group's matrix to the canvas
|
|
SkMatrix stackedMatrix;
|
|
const GroupProperties& prop = useStagingData ? mStagingProperties : mProperties;
|
|
getLocalMatrix(&stackedMatrix, prop);
|
|
outCanvas->concat(stackedMatrix);
|
|
// Draw the group tree in the same order as the XML file.
|
|
for (auto& child : mChildren) {
|
|
child->draw(outCanvas, useStagingData);
|
|
}
|
|
// Restore the previous clip and matrix information.
|
|
}
|
|
|
|
void Group::dump() {
|
|
ALOGD("Group %s has %zu children: ", mName.c_str(), mChildren.size());
|
|
ALOGD("Group translateX, Y : %f, %f, scaleX, Y: %f, %f", mProperties.getTranslateX(),
|
|
mProperties.getTranslateY(), mProperties.getScaleX(), mProperties.getScaleY());
|
|
for (size_t i = 0; i < mChildren.size(); i++) {
|
|
mChildren[i]->dump();
|
|
}
|
|
}
|
|
|
|
void Group::syncProperties() {
|
|
// Copy over the dirty staging properties
|
|
if (mStagingPropertiesDirty) {
|
|
mProperties.syncProperties(mStagingProperties);
|
|
} else {
|
|
mStagingProperties.syncProperties(mProperties);
|
|
}
|
|
mStagingPropertiesDirty = false;
|
|
for (auto& child : mChildren) {
|
|
child->syncProperties();
|
|
}
|
|
}
|
|
|
|
void Group::getLocalMatrix(SkMatrix* outMatrix, const GroupProperties& properties) {
|
|
outMatrix->reset();
|
|
// TODO: use rotate(mRotate, mPivotX, mPivotY) and scale with pivot point, instead of
|
|
// translating to pivot for rotating and scaling, then translating back.
|
|
outMatrix->postTranslate(-properties.getPivotX(), -properties.getPivotY());
|
|
outMatrix->postScale(properties.getScaleX(), properties.getScaleY());
|
|
outMatrix->postRotate(properties.getRotation(), 0, 0);
|
|
outMatrix->postTranslate(properties.getTranslateX() + properties.getPivotX(),
|
|
properties.getTranslateY() + properties.getPivotY());
|
|
}
|
|
|
|
void Group::addChild(Node* child) {
|
|
mChildren.emplace_back(child);
|
|
if (mPropertyChangedListener != nullptr) {
|
|
child->setPropertyChangedListener(mPropertyChangedListener);
|
|
}
|
|
}
|
|
|
|
bool Group::GroupProperties::copyProperties(float* outProperties, int length) const {
|
|
int propertyCount = static_cast<int>(Property::count);
|
|
if (length != propertyCount) {
|
|
LOG_ALWAYS_FATAL("Properties needs exactly %d bytes, a byte array of size %d is provided",
|
|
propertyCount, length);
|
|
return false;
|
|
}
|
|
|
|
PrimitiveFields* out = reinterpret_cast<PrimitiveFields*>(outProperties);
|
|
*out = mPrimitiveFields;
|
|
return true;
|
|
}
|
|
|
|
// TODO: Consider animating the properties as float pointers
|
|
// Called on render thread
|
|
float Group::GroupProperties::getPropertyValue(int propertyId) const {
|
|
Property currentProperty = static_cast<Property>(propertyId);
|
|
switch (currentProperty) {
|
|
case Property::rotate:
|
|
return getRotation();
|
|
case Property::pivotX:
|
|
return getPivotX();
|
|
case Property::pivotY:
|
|
return getPivotY();
|
|
case Property::scaleX:
|
|
return getScaleX();
|
|
case Property::scaleY:
|
|
return getScaleY();
|
|
case Property::translateX:
|
|
return getTranslateX();
|
|
case Property::translateY:
|
|
return getTranslateY();
|
|
default:
|
|
LOG_ALWAYS_FATAL("Invalid property index: %d", propertyId);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Called on render thread
|
|
void Group::GroupProperties::setPropertyValue(int propertyId, float value) {
|
|
Property currentProperty = static_cast<Property>(propertyId);
|
|
switch (currentProperty) {
|
|
case Property::rotate:
|
|
setRotation(value);
|
|
break;
|
|
case Property::pivotX:
|
|
setPivotX(value);
|
|
break;
|
|
case Property::pivotY:
|
|
setPivotY(value);
|
|
break;
|
|
case Property::scaleX:
|
|
setScaleX(value);
|
|
break;
|
|
case Property::scaleY:
|
|
setScaleY(value);
|
|
break;
|
|
case Property::translateX:
|
|
setTranslateX(value);
|
|
break;
|
|
case Property::translateY:
|
|
setTranslateY(value);
|
|
break;
|
|
default:
|
|
LOG_ALWAYS_FATAL("Invalid property index: %d", propertyId);
|
|
}
|
|
}
|
|
|
|
bool Group::isValidProperty(int propertyId) {
|
|
return GroupProperties::isValidProperty(propertyId);
|
|
}
|
|
|
|
bool Group::GroupProperties::isValidProperty(int propertyId) {
|
|
return propertyId >= 0 && propertyId < static_cast<int>(Property::count);
|
|
}
|
|
|
|
int Tree::draw(Canvas* outCanvas, SkColorFilter* colorFilter, const SkRect& bounds,
|
|
bool needsMirroring, bool canReuseCache) {
|
|
// The imageView can scale the canvas in different ways, in order to
|
|
// avoid blurry scaling, we have to draw into a bitmap with exact pixel
|
|
// size first. This bitmap size is determined by the bounds and the
|
|
// canvas scale.
|
|
SkMatrix canvasMatrix;
|
|
outCanvas->getMatrix(&canvasMatrix);
|
|
float canvasScaleX = 1.0f;
|
|
float canvasScaleY = 1.0f;
|
|
if (canvasMatrix.getSkewX() == 0 && canvasMatrix.getSkewY() == 0) {
|
|
// Only use the scale value when there's no skew or rotation in the canvas matrix.
|
|
// TODO: Add a cts test for drawing VD on a canvas with negative scaling factors.
|
|
canvasScaleX = fabs(canvasMatrix.getScaleX());
|
|
canvasScaleY = fabs(canvasMatrix.getScaleY());
|
|
}
|
|
int scaledWidth = (int)(bounds.width() * canvasScaleX);
|
|
int scaledHeight = (int)(bounds.height() * canvasScaleY);
|
|
scaledWidth = std::min(Tree::MAX_CACHED_BITMAP_SIZE, scaledWidth);
|
|
scaledHeight = std::min(Tree::MAX_CACHED_BITMAP_SIZE, scaledHeight);
|
|
|
|
if (scaledWidth <= 0 || scaledHeight <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
mStagingProperties.setScaledSize(scaledWidth, scaledHeight);
|
|
int saveCount = outCanvas->save(SaveFlags::MatrixClip);
|
|
outCanvas->translate(bounds.fLeft, bounds.fTop);
|
|
|
|
// Handle RTL mirroring.
|
|
if (needsMirroring) {
|
|
outCanvas->translate(bounds.width(), 0);
|
|
outCanvas->scale(-1.0f, 1.0f);
|
|
}
|
|
mStagingProperties.setColorFilter(colorFilter);
|
|
|
|
// At this point, canvas has been translated to the right position.
|
|
// And we use this bound for the destination rect for the drawBitmap, so
|
|
// we offset to (0, 0);
|
|
SkRect tmpBounds = bounds;
|
|
tmpBounds.offsetTo(0, 0);
|
|
mStagingProperties.setBounds(tmpBounds);
|
|
outCanvas->drawVectorDrawable(this);
|
|
outCanvas->restoreToCount(saveCount);
|
|
return scaledWidth * scaledHeight;
|
|
}
|
|
|
|
void Tree::drawStaging(Canvas* outCanvas) {
|
|
bool redrawNeeded = allocateBitmapIfNeeded(mStagingCache, mStagingProperties.getScaledWidth(),
|
|
mStagingProperties.getScaledHeight());
|
|
// draw bitmap cache
|
|
if (redrawNeeded || mStagingCache.dirty) {
|
|
updateBitmapCache(*mStagingCache.bitmap, true);
|
|
mStagingCache.dirty = false;
|
|
}
|
|
|
|
SkPaint paint;
|
|
getPaintFor(&paint, mStagingProperties);
|
|
outCanvas->drawBitmap(*mStagingCache.bitmap, 0, 0, mStagingCache.bitmap->width(),
|
|
mStagingCache.bitmap->height(), mStagingProperties.getBounds().left(),
|
|
mStagingProperties.getBounds().top(),
|
|
mStagingProperties.getBounds().right(),
|
|
mStagingProperties.getBounds().bottom(), &paint);
|
|
}
|
|
|
|
void Tree::getPaintFor(SkPaint* outPaint, const TreeProperties &prop) const {
|
|
// HWUI always draws VD with bilinear filtering.
|
|
outPaint->setFilterQuality(kLow_SkFilterQuality);
|
|
if (prop.getColorFilter() != nullptr) {
|
|
outPaint->setColorFilter(sk_ref_sp(prop.getColorFilter()));
|
|
}
|
|
outPaint->setAlpha(prop.getRootAlpha() * 255);
|
|
}
|
|
|
|
Bitmap& Tree::getBitmapUpdateIfDirty() {
|
|
bool redrawNeeded = allocateBitmapIfNeeded(mCache, mProperties.getScaledWidth(),
|
|
mProperties.getScaledHeight());
|
|
if (redrawNeeded || mCache.dirty) {
|
|
updateBitmapCache(*mCache.bitmap, false);
|
|
mCache.dirty = false;
|
|
}
|
|
return *mCache.bitmap;
|
|
}
|
|
|
|
void Tree::updateCache(sp<skiapipeline::VectorDrawableAtlas>& atlas, GrContext* context) {
|
|
SkRect dst;
|
|
sk_sp<SkSurface> surface = mCache.getSurface(&dst);
|
|
bool canReuseSurface = surface && dst.width() >= mProperties.getScaledWidth() &&
|
|
dst.height() >= mProperties.getScaledHeight();
|
|
if (!canReuseSurface) {
|
|
int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth());
|
|
int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight());
|
|
auto atlasEntry = atlas->requestNewEntry(scaledWidth, scaledHeight, context);
|
|
if (INVALID_ATLAS_KEY != atlasEntry.key) {
|
|
dst = atlasEntry.rect;
|
|
surface = atlasEntry.surface;
|
|
mCache.setAtlas(atlas, atlasEntry.key);
|
|
} else {
|
|
// don't draw, if we failed to allocate an offscreen buffer
|
|
mCache.clear();
|
|
surface.reset();
|
|
}
|
|
}
|
|
if (!canReuseSurface || mCache.dirty) {
|
|
if (surface) {
|
|
Bitmap& bitmap = getBitmapUpdateIfDirty();
|
|
SkBitmap skiaBitmap;
|
|
bitmap.getSkBitmap(&skiaBitmap);
|
|
surface->writePixels(skiaBitmap, dst.fLeft, dst.fTop);
|
|
}
|
|
mCache.dirty = false;
|
|
}
|
|
}
|
|
|
|
void Tree::Cache::setAtlas(sp<skiapipeline::VectorDrawableAtlas> newAtlas,
|
|
skiapipeline::AtlasKey newAtlasKey) {
|
|
LOG_ALWAYS_FATAL_IF(newAtlasKey == INVALID_ATLAS_KEY);
|
|
clear();
|
|
mAtlas = newAtlas;
|
|
mAtlasKey = newAtlasKey;
|
|
}
|
|
|
|
sk_sp<SkSurface> Tree::Cache::getSurface(SkRect* bounds) {
|
|
sk_sp<SkSurface> surface;
|
|
sp<skiapipeline::VectorDrawableAtlas> atlas = mAtlas.promote();
|
|
if (atlas.get() && mAtlasKey != INVALID_ATLAS_KEY) {
|
|
auto atlasEntry = atlas->getEntry(mAtlasKey);
|
|
*bounds = atlasEntry.rect;
|
|
surface = atlasEntry.surface;
|
|
mAtlasKey = atlasEntry.key;
|
|
}
|
|
|
|
return surface;
|
|
}
|
|
|
|
void Tree::Cache::clear() {
|
|
sp<skiapipeline::VectorDrawableAtlas> lockAtlas = mAtlas.promote();
|
|
if (lockAtlas.get()) {
|
|
lockAtlas->releaseEntry(mAtlasKey);
|
|
}
|
|
mAtlas = nullptr;
|
|
mAtlasKey = INVALID_ATLAS_KEY;
|
|
}
|
|
|
|
void Tree::draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& inPaint) {
|
|
if (canvas->quickReject(bounds)) {
|
|
// The RenderNode is on screen, but the AVD is not.
|
|
return;
|
|
}
|
|
|
|
// Update the paint for any animatable properties
|
|
SkPaint paint = inPaint;
|
|
paint.setAlpha(mProperties.getRootAlpha() * 255);
|
|
|
|
if (canvas->getGrContext() == nullptr) {
|
|
// Recording to picture, don't use the SkSurface which won't work off of renderthread.
|
|
Bitmap& bitmap = getBitmapUpdateIfDirty();
|
|
SkBitmap skiaBitmap;
|
|
bitmap.getSkBitmap(&skiaBitmap);
|
|
|
|
int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth());
|
|
int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight());
|
|
canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds,
|
|
&paint, SkCanvas::kFast_SrcRectConstraint);
|
|
return;
|
|
}
|
|
|
|
SkRect src;
|
|
sk_sp<SkSurface> vdSurface = mCache.getSurface(&src);
|
|
if (vdSurface) {
|
|
canvas->drawImageRect(vdSurface->makeImageSnapshot().get(), src, bounds, &paint,
|
|
SkCanvas::kFast_SrcRectConstraint);
|
|
} else {
|
|
// Handle the case when VectorDrawableAtlas has been destroyed, because of memory pressure.
|
|
// We render the VD into a temporary standalone buffer and mark the frame as dirty. Next
|
|
// frame will be cached into the atlas.
|
|
Bitmap& bitmap = getBitmapUpdateIfDirty();
|
|
SkBitmap skiaBitmap;
|
|
bitmap.getSkBitmap(&skiaBitmap);
|
|
|
|
int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth());
|
|
int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight());
|
|
canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds,
|
|
&paint, SkCanvas::kFast_SrcRectConstraint);
|
|
mCache.clear();
|
|
markDirty();
|
|
}
|
|
}
|
|
|
|
void Tree::updateBitmapCache(Bitmap& bitmap, bool useStagingData) {
|
|
SkBitmap outCache;
|
|
bitmap.getSkBitmap(&outCache);
|
|
int cacheWidth = outCache.width();
|
|
int cacheHeight = outCache.height();
|
|
ATRACE_FORMAT("VectorDrawable repaint %dx%d", cacheWidth, cacheHeight);
|
|
outCache.eraseColor(SK_ColorTRANSPARENT);
|
|
SkCanvas outCanvas(outCache);
|
|
float viewportWidth =
|
|
useStagingData ? mStagingProperties.getViewportWidth() : mProperties.getViewportWidth();
|
|
float viewportHeight = useStagingData ? mStagingProperties.getViewportHeight()
|
|
: mProperties.getViewportHeight();
|
|
float scaleX = cacheWidth / viewportWidth;
|
|
float scaleY = cacheHeight / viewportHeight;
|
|
outCanvas.scale(scaleX, scaleY);
|
|
mRootNode->draw(&outCanvas, useStagingData);
|
|
}
|
|
|
|
bool Tree::allocateBitmapIfNeeded(Cache& cache, int width, int height) {
|
|
if (!canReuseBitmap(cache.bitmap.get(), width, height)) {
|
|
SkImageInfo info = SkImageInfo::MakeN32(width, height, kPremul_SkAlphaType);
|
|
cache.bitmap = Bitmap::allocateHeapBitmap(info);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Tree::canReuseBitmap(Bitmap* bitmap, int width, int height) {
|
|
return bitmap && width <= bitmap->width() && height <= bitmap->height();
|
|
}
|
|
|
|
void Tree::onPropertyChanged(TreeProperties* prop) {
|
|
if (prop == &mStagingProperties) {
|
|
mStagingCache.dirty = true;
|
|
} else {
|
|
mCache.dirty = true;
|
|
}
|
|
}
|
|
|
|
class MinMaxAverage {
|
|
public:
|
|
void add(float sample) {
|
|
if (mCount == 0) {
|
|
mMin = sample;
|
|
mMax = sample;
|
|
} else {
|
|
mMin = std::min(mMin, sample);
|
|
mMax = std::max(mMax, sample);
|
|
}
|
|
mTotal += sample;
|
|
mCount++;
|
|
}
|
|
|
|
float average() { return mTotal / mCount; }
|
|
|
|
float min() { return mMin; }
|
|
|
|
float max() { return mMax; }
|
|
|
|
float delta() { return mMax - mMin; }
|
|
|
|
private:
|
|
float mMin = 0.0f;
|
|
float mMax = 0.0f;
|
|
float mTotal = 0.0f;
|
|
int mCount = 0;
|
|
};
|
|
|
|
BitmapPalette Tree::computePalette() {
|
|
// TODO Cache this and share the code with Bitmap.cpp
|
|
|
|
ATRACE_CALL();
|
|
|
|
// TODO: This calculation of converting to HSV & tracking min/max is probably overkill
|
|
// Experiment with something simpler since we just want to figure out if it's "color-ful"
|
|
// and then the average perceptual lightness.
|
|
|
|
MinMaxAverage hue, saturation, value;
|
|
int sampledCount = 0;
|
|
|
|
// Sample a grid of 100 pixels to get an overall estimation of the colors in play
|
|
mRootNode->forEachFillColor([&](SkColor color) {
|
|
if (SkColorGetA(color) < 75) {
|
|
return;
|
|
}
|
|
sampledCount++;
|
|
float hsv[3];
|
|
SkColorToHSV(color, hsv);
|
|
hue.add(hsv[0]);
|
|
saturation.add(hsv[1]);
|
|
value.add(hsv[2]);
|
|
});
|
|
|
|
if (sampledCount == 0) {
|
|
ALOGV("VectorDrawable is mostly translucent");
|
|
return BitmapPalette::Unknown;
|
|
}
|
|
|
|
ALOGV("samples = %d, hue [min = %f, max = %f, avg = %f]; saturation [min = %f, max = %f, avg = "
|
|
"%f]; value [min = %f, max = %f, avg = %f]",
|
|
sampledCount, hue.min(), hue.max(), hue.average(), saturation.min(), saturation.max(),
|
|
saturation.average(), value.min(), value.max(), value.average());
|
|
|
|
if (hue.delta() <= 20 && saturation.delta() <= .1f) {
|
|
if (value.average() >= .5f) {
|
|
return BitmapPalette::Light;
|
|
} else {
|
|
return BitmapPalette::Dark;
|
|
}
|
|
}
|
|
return BitmapPalette::Unknown;
|
|
}
|
|
|
|
} // namespace VectorDrawable
|
|
|
|
} // namespace uirenderer
|
|
} // namespace android
|