/*
* 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.drawee.debug;
import javax.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.drawee.drawable.ScalingUtils.ScaleType;
/**
* Drawee Controller overlay that displays debug information.
*/
public class DebugControllerOverlayDrawable extends Drawable {
private static final String NO_CONTROLLER_ID = "none";
// Green if the image dimensions are OK
@VisibleForTesting
static final int OVERLAY_COLOR_IMAGE_OK = 0x664CAF50;
// Orange if the image dimensions are a bit off
@VisibleForTesting
static final int OVERLAY_COLOR_IMAGE_ALMOST_OK = 0x66FF9800;
// Red if the image dimensions are too far off
@VisibleForTesting
static final int OVERLAY_COLOR_IMAGE_NOT_OK = 0x66F44336;
// Values are given in per cent. E.g. 0.1 means 10% smaller or larger.
private static final float IMAGE_SIZE_THRESHOLD_OK = 0.1f;
private static final float IMAGE_SIZE_THRESHOLD_NOT_OK = 0.5f;
private static final int OUTLINE_COLOR = 0xFFFF9800;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int OUTLINE_STROKE_WIDTH_PX = 2;
private static final int MAX_TEXT_SIZE_PX = 40;
private static final int MIN_TEXT_SIZE_PX = 12;
private static final int TEXT_LINE_SPACING_PX = 8;
private static final int TEXT_PADDING_PX = 10;
// Debug-text dependent parameters
private static final int MAX_NUMBER_OF_LINES = 7;
private static final int MAX_LINE_WIDTH_EM = 7;
// General information
private String mControllerId;
private int mWidthPx;
private int mHeightPx;
private int mImageSizeBytes;
private String mImageFormat;
private ScaleType mScaleType;
// Animations
private int mFrameCount;
private int mLoopCount;
// Text gravity
private int mTextGravity = Gravity.BOTTOM;
// Internal helpers
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Matrix mMatrix = new Matrix(); // avoid local allocation
private final Rect mRect = new Rect(); // avoid local allocation
private final RectF mRectF = new RectF(); // avoid local allocation
private int mStartTextXPx;
private int mStartTextYPx;
private int mLineIncrementPx;
private int mCurrentTextXPx;
private int mCurrentTextYPx;
public DebugControllerOverlayDrawable() {
reset();
}
public void reset() {
mWidthPx = -1;
mHeightPx = -1;
mImageSizeBytes = -1;
mFrameCount = -1;
mLoopCount = -1;
mImageFormat = null;
setControllerId(null);
invalidateSelf();
}
/**
* The text gravity / direction for the debug text.
* Currently supported: {@link Gravity#BOTTOM} and {@link Gravity#TOP}.
* If bottom is used, the text lines will also be drawn from bottom to top.
* Default: bottom
* @param textGravity the text gravity to use
*/
public void setTextGravity(int textGravity) {
mTextGravity = textGravity;
invalidateSelf();
}
public void setControllerId(@Nullable String controllerId) {
mControllerId = controllerId != null ? controllerId : NO_CONTROLLER_ID;
invalidateSelf();
}
public void setDimensions(int widthPx, int heightPx) {
mWidthPx = widthPx;
mHeightPx = heightPx;
invalidateSelf();
}
public void setAnimationInfo(int frameCount, int loopCount) {
mFrameCount = frameCount;
mLoopCount = loopCount;
invalidateSelf();
}
/**
*
* @param imageSizeBytes the image size in bytes
*/
public void setImageSize(int imageSizeBytes) {
mImageSizeBytes = imageSizeBytes;
}
public void setImageFormat(@Nullable String imageFormat) {
mImageFormat = imageFormat;
}
public void setScaleType(ScaleType scaleType) {
mScaleType = scaleType;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
// Update the text parameters since the size changed. If you modify the debug text, make sure
// to also update MAX_NUMBER_OF_LINES and MAX_LINE_WIDTH_EM. The line width has been estimated
// for a reasonable max line width on average.
prepareDebugTextParameters(bounds, MAX_NUMBER_OF_LINES, MAX_LINE_WIDTH_EM);
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
// Draw bounding box
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(OUTLINE_STROKE_WIDTH_PX);
mPaint.setColor(OUTLINE_COLOR);
canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.bottom, mPaint);
// Draw overlay
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(determineOverlayColor(mWidthPx, mHeightPx, mScaleType));
canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.bottom, mPaint);
// Draw text
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(0);
mPaint.setColor(TEXT_COLOR);
// Reset the test position
mCurrentTextXPx = mStartTextXPx;
mCurrentTextYPx = mStartTextYPx;
addDebugText(canvas, "ID: %s", mControllerId);
addDebugText(canvas, "D: %dx%d", bounds.width(), bounds.height());
addDebugText(canvas, "I: %dx%d", mWidthPx, mHeightPx);
addDebugText(canvas, "I: %d KiB", (mImageSizeBytes / 1024));
if (mImageFormat != null) {
addDebugText(canvas, "i format: %s", mImageFormat);
}
if (mFrameCount > 0) {
addDebugText(canvas, "anim: f %d, l %d", mFrameCount, mLoopCount);
}
if (mScaleType != null) {
addDebugText(canvas, "scale: %s", mScaleType);
}
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private void prepareDebugTextParameters(
Rect bounds,
int numberOfLines,
int maxLineLengthEm) {
int textSizePx = Math.min(bounds.width() / maxLineLengthEm, bounds.height() / numberOfLines);
textSizePx = Math.min(MAX_TEXT_SIZE_PX, Math.max(MIN_TEXT_SIZE_PX, textSizePx));
mPaint.setTextSize(textSizePx);
mLineIncrementPx = textSizePx + TEXT_LINE_SPACING_PX;
if (mTextGravity == Gravity.BOTTOM) {
mLineIncrementPx *= -1;
}
mStartTextXPx = bounds.left + TEXT_PADDING_PX;
mStartTextYPx = mTextGravity == Gravity.BOTTOM
? bounds.bottom - TEXT_PADDING_PX
: bounds.top + TEXT_PADDING_PX + MIN_TEXT_SIZE_PX;
}
private void addDebugText(Canvas canvas, String text, @Nullable Object... args) {
if (args == null) {
canvas.drawText(text, mCurrentTextXPx, mCurrentTextYPx, mPaint);
} else {
canvas.drawText(String.format(text, args), mCurrentTextXPx, mCurrentTextYPx, mPaint);
}
mCurrentTextYPx += mLineIncrementPx;
}
@VisibleForTesting
int determineOverlayColor(
int imageWidth,
int imageHeight,
@Nullable ScaleType scaleType) {
int visibleDrawnAreaWidth = getBounds().width();
int visibleDrawnAreaHeight = getBounds().height();
if (visibleDrawnAreaWidth <= 0 ||
visibleDrawnAreaHeight <= 0 ||
imageWidth <= 0 ||
imageHeight <= 0) {
return OVERLAY_COLOR_IMAGE_NOT_OK;
}
if (scaleType != null) {
// Apply optional scale type in order to get boundaries of the actual area to be filled
mRect.left = mRect.top = 0;
mRect.right = visibleDrawnAreaWidth;
mRect.bottom = visibleDrawnAreaHeight;
mMatrix.reset();
// We can ignore the focus point as it has no influence on the scale, but only the translation
scaleType.getTransform(mMatrix, mRect, imageWidth, imageHeight, 0f, 0f);
mRectF.left = mRectF.top = 0;
mRectF.right = imageWidth;
mRectF.bottom = imageHeight;
mMatrix.mapRect(mRectF);
final int drawnAreaWidth = (int) mRectF.width();
final int drawnAreaHeight = (int) mRectF.height();
visibleDrawnAreaWidth = Math.min(visibleDrawnAreaWidth, drawnAreaWidth);
visibleDrawnAreaHeight = Math.min(visibleDrawnAreaHeight, drawnAreaHeight);
}
// Update the thresholds for the overlay color
float scaledImageWidthThresholdOk = visibleDrawnAreaWidth * IMAGE_SIZE_THRESHOLD_OK;
float scaledImageWidthThresholdNotOk = visibleDrawnAreaWidth * IMAGE_SIZE_THRESHOLD_NOT_OK;
float scaledImageHeightThresholdOk = visibleDrawnAreaHeight * IMAGE_SIZE_THRESHOLD_OK;
float scaledImageHeightThresholdNotOk = visibleDrawnAreaHeight * IMAGE_SIZE_THRESHOLD_NOT_OK;
// Calculate the dimension differences
int absWidthDifference = Math.abs(imageWidth - visibleDrawnAreaWidth);
int absHeightDifference = Math.abs(imageHeight - visibleDrawnAreaHeight);
// Return corresponding color
if (absWidthDifference < scaledImageWidthThresholdOk &&
absHeightDifference < scaledImageHeightThresholdOk) {
return OVERLAY_COLOR_IMAGE_OK;
} else if (absWidthDifference < scaledImageWidthThresholdNotOk &&
absHeightDifference < scaledImageHeightThresholdNotOk) {
return OVERLAY_COLOR_IMAGE_ALMOST_OK;
}
return OVERLAY_COLOR_IMAGE_NOT_OK;
}
}