/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
/**
* Base class for all DrawCommands. Becomes immutable once it has its bounds set. Until then, a
* subclass is able to mutate any of its properties (e.g. updating Layout in DrawTextLayout).
*
* The idea is to be able to reuse unmodified objects when we build up DrawCommands before we ship
* them to UI thread, but we can only do that if DrawCommands are immutable.
*/
/* package */ abstract class AbstractDrawCommand extends DrawCommand implements Cloneable {
private float mLeft;
private float mTop;
private float mRight;
private float mBottom;
private boolean mFrozen;
protected boolean mNeedsClipping;
private float mClipLeft;
private float mClipTop;
private float mClipRight;
private float mClipBottom;
// Used to draw highlights in debug draw.
private static Paint sDebugHighlightRed;
private static Paint sDebugHighlightYellow;
private static Paint sDebugHighlightOverlayText;
public final boolean clipBoundsMatch(
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
return mClipLeft == clipLeft && mClipTop == clipTop
&& mClipRight == clipRight && mClipBottom == clipBottom;
}
protected final void setClipBounds(
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
mClipLeft = clipLeft;
mClipTop = clipTop;
mClipRight = clipRight;
mClipBottom = clipBottom;
// We put this check here to not clip when we have the default [-infinity, infinity] bounds,
// since clipRect in those cases is essentially no-op anyway. This is needed to fix a bug that
// shows up during screenshot testing. Note that checking one side is enough, since if one side
// is infinite, all sides will be infinite, since we only set infinite for all sides at the
// same time - conversely, if one side is finite, all sides will be finite.
mNeedsClipping = mClipLeft != Float.NEGATIVE_INFINITY;
}
public final float getClipLeft() {
return mClipLeft;
}
public final float getClipTop() {
return mClipTop;
}
public final float getClipRight() {
return mClipRight;
}
public final float getClipBottom() {
return mClipBottom;
}
protected void applyClipping(Canvas canvas) {
canvas.clipRect(mClipLeft, mClipTop, mClipRight, mClipBottom);
}
/**
* Don't override this unless you need to do custom clipping in a draw command. Otherwise just
* override onPreDraw and onDraw.
*/
@Override
public void draw(FlatViewGroup parent, Canvas canvas) {
onPreDraw(parent, canvas);
if (mNeedsClipping && shouldClip()) {
canvas.save(Canvas.CLIP_SAVE_FLAG);
applyClipping(canvas);
onDraw(canvas);
canvas.restore();
} else {
onDraw(canvas);
}
}
protected static int getDebugBorderColor() {
return Color.CYAN;
}
protected String getDebugName() {
return getClass().getSimpleName().substring(4);
}
private void initDebugHighlightResources(FlatViewGroup parent) {
if (sDebugHighlightRed == null) {
sDebugHighlightRed = new Paint();
sDebugHighlightRed.setARGB(75, 255, 0, 0);
}
if (sDebugHighlightYellow == null) {
sDebugHighlightYellow = new Paint();
sDebugHighlightYellow.setARGB(100, 255, 204, 0);
}
if (sDebugHighlightOverlayText == null) {
sDebugHighlightOverlayText = new Paint();
sDebugHighlightOverlayText.setAntiAlias(true);
sDebugHighlightOverlayText.setARGB(200, 50, 50, 50);
sDebugHighlightOverlayText.setTextAlign(Paint.Align.RIGHT);
sDebugHighlightOverlayText.setTypeface(Typeface.MONOSPACE);
sDebugHighlightOverlayText.setTextSize(parent.dipsToPixels(9));
}
}
private void debugDrawHighlightRect(Canvas canvas, Paint paint, String text) {
canvas.drawRect(getLeft(), getTop(), getRight(), getBottom(), paint);
canvas.drawText(text, getRight() - 5, getBottom() - 5, sDebugHighlightOverlayText);
}
protected void debugDrawWarningHighlight(Canvas canvas, String text) {
debugDrawHighlightRect(canvas, sDebugHighlightRed, text);
}
protected void debugDrawCautionHighlight(Canvas canvas, String text) {
debugDrawHighlightRect(canvas, sDebugHighlightYellow, text);
}
@Override
public final void debugDraw(FlatViewGroup parent, Canvas canvas) {
onDebugDraw(parent, canvas);
if (FlatViewGroup.DEBUG_HIGHLIGHT_PERFORMANCE_ISSUES) {
initDebugHighlightResources(parent);
onDebugDrawHighlight(canvas);
}
}
protected void onDebugDraw(FlatViewGroup parent, Canvas canvas) {
parent.debugDrawNamedRect(
canvas,
getDebugBorderColor(),
getDebugName(),
mLeft,
mTop,
mRight,
mBottom);
}
protected void onDebugDrawHighlight(Canvas canvas) {
}
protected void onPreDraw(FlatViewGroup parent, Canvas canvas) {
}
/**
* Updates boundaries of the AbstractDrawCommand and freezes it.
* Will return a frozen copy if the current AbstractDrawCommand cannot be mutated.
*
* This should not be called on a DrawView, as the DrawView is modified on UI thread. Use
* DrawView.collectDrawView instead to avoid race conditions.
*/
public AbstractDrawCommand updateBoundsAndFreeze(
float left,
float top,
float right,
float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
if (mFrozen) {
// see if we can reuse it
boolean boundsMatch = boundsMatch(left, top, right, bottom);
boolean clipBoundsMatch = clipBoundsMatch(clipLeft, clipTop, clipRight, clipBottom);
if (boundsMatch && clipBoundsMatch) {
return this;
}
try {
AbstractDrawCommand copy = (AbstractDrawCommand) clone();
if (!boundsMatch) {
copy.setBounds(left, top, right, bottom);
}
if (!clipBoundsMatch) {
copy.setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
}
return copy;
} catch (CloneNotSupportedException e) {
// This should not happen since AbstractDrawCommand implements Cloneable
throw new RuntimeException(e);
}
}
setBounds(left, top, right, bottom);
setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
mFrozen = true;
return this;
}
/**
* Returns a non-frozen shallow copy of AbstractDrawCommand as defined by {@link Object#clone()}.
*/
public final AbstractDrawCommand mutableCopy() {
try {
AbstractDrawCommand copy = (AbstractDrawCommand) super.clone();
copy.mFrozen = false;
return copy;
} catch (CloneNotSupportedException e) {
// should not happen since we implement Cloneable
throw new RuntimeException(e);
}
}
/**
* Returns whether this object was frozen and thus cannot be mutated.
*/
public final boolean isFrozen() {
return mFrozen;
}
/**
* Mark this object as frozen, indicating that it should not be mutated.
*/
public final void freeze() {
mFrozen = true;
}
/**
* Left position of this DrawCommand relative to the hosting View.
*/
public final float getLeft() {
return mLeft;
}
/**
* Top position of this DrawCommand relative to the hosting View.
*/
public final float getTop() {
return mTop;
}
/**
* Right position of this DrawCommand relative to the hosting View.
*/
public final float getRight() {
return mRight;
}
/**
* Bottom position of this DrawCommand relative to the hosting View.
*/
public final float getBottom() {
return mBottom;
}
protected abstract void onDraw(Canvas canvas);
protected boolean shouldClip() {
return mLeft < getClipLeft() || mTop < getClipTop() ||
mRight > getClipRight() || mBottom > getClipBottom();
}
protected void onBoundsChanged() {
}
/**
* Updates boundaries of this DrawCommand.
*/
protected final void setBounds(float left, float top, float right, float bottom) {
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
onBoundsChanged();
}
/**
* Returns true if boundaries match and don't need to be updated. False otherwise.
*/
protected final boolean boundsMatch(float left, float top, float right, float bottom) {
return mLeft == left && mTop == top && mRight == right && mBottom == bottom;
}
}