blob: 2b518ccdd608a6eb4069c57d105d8172ce5092a4 [file] [log] [blame]
// Copyright 2018 The Feed Authors.
//
// 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.
package com.google.android.libraries.feed.piet.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import com.google.android.libraries.feed.common.ui.LayoutUtils;
import com.google.search.now.ui.piet.StylesProto.Borders;
import com.google.search.now.ui.piet.StylesProto.Borders.Edges;
/**
* Shape used to draw borders. Uses offsets to push the border out of the drawing bounds if it is
* not specified.
*/
public class BorderDrawable extends ShapeDrawable {
private final float[] cornerRadii;
private final int initialWidth;
private final int initialHeight;
private final boolean hasLeftBorder;
private final boolean hasRightBorder;
private final boolean hasTopBorder;
private final boolean hasBottomBorder;
private final int offsetToHideLeft;
private final int offsetToHideRight;
private final int offsetToHideTop;
private final int offsetToHideBottom;
private final int borderWidth;
public BorderDrawable(Context context, Borders borders, float[] cornerRadii, boolean isRtL) {
this(context, borders, cornerRadii, isRtL, /* width= */ 0, /* height= */ 0);
}
// Doesn't like calls to getPaint()
@SuppressWarnings("initialization")
public BorderDrawable(
Context context, Borders borders, float[] cornerRadii, boolean isRtL, int width, int height) {
super(new RoundRectShape(cornerRadii, null, null));
this.cornerRadii = cornerRadii;
this.initialWidth = width;
this.initialHeight = height;
borderWidth = (int) LayoutUtils.dpToPx(borders.getWidth(), context);
// Calculate the offsets which push the border outside the view, making it invisible
int bitmask = borders.getBitmask();
if (bitmask == 0 || bitmask == 15) {
// All borders are visible
hasLeftBorder = true;
hasRightBorder = true;
hasTopBorder = true;
hasBottomBorder = true;
offsetToHideLeft = 0;
offsetToHideRight = 0;
offsetToHideTop = 0;
offsetToHideBottom = 0;
} else {
int leftEdge = isRtL ? Edges.END.getNumber() : Edges.START.getNumber();
int rightEdge = isRtL ? Edges.START.getNumber() : Edges.END.getNumber();
hasLeftBorder = (bitmask & leftEdge) != 0;
hasRightBorder = (bitmask & rightEdge) != 0;
hasTopBorder = (bitmask & Edges.TOP.getNumber()) != 0;
hasBottomBorder = (bitmask & Edges.BOTTOM.getNumber()) != 0;
offsetToHideLeft = hasLeftBorder ? 0 : -borderWidth;
offsetToHideRight = hasRightBorder ? 0 : borderWidth;
offsetToHideTop = hasTopBorder ? 0 : -borderWidth;
offsetToHideBottom = hasBottomBorder ? 0 : borderWidth;
}
getPaint().setStyle(Paint.Style.STROKE);
// Multiply the width by two - the centerline of the stroke will be the edge of the view, so
// half of the stroke will be outside the view. In order for the visible portion to have the
// correct width, the full stroke needs to be twice as wide.
// For rounded corners, this relies on the containing FrameLayout to crop the outside half of
// the rounded border; otherwise, the border would get thicker on the corners.
getPaint().setStrokeWidth(borderWidth * 2);
getPaint().setColor(borders.getColor());
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(
left + offsetToHideLeft,
top + offsetToHideTop,
right + offsetToHideRight,
bottom + offsetToHideBottom);
}
@Override
public void setBounds(Rect bounds) {
setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
}
@Override
protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
if (Build.VERSION.SDK_INT < VERSION_CODES.KITKAT && hasLargeRadius()) {
drawBorderWithPath(canvas, paint);
} else {
super.onDraw(shape, canvas, paint);
}
}
/**
* Checks if the radius is larger than half the width or height. Only applies to elements with
* rounded corners.
*/
private boolean hasLargeRadius() {
if (initialWidth == 0 || initialHeight == 0) {
return false;
}
int radius = 0;
for (float cornerRadius : cornerRadii) {
if (cornerRadius != 0) {
radius = (int) cornerRadius;
break;
}
}
if (radius == 0) {
return false;
}
if (radius > (initialWidth / 2) || radius > (initialHeight / 2)) {
return true;
}
return false;
}
/**
* Draws the border with a {@link Path} instead of a {@link RoundRectShape}. This is used because
* in JellyBean, RoundRectShape clamps the rounded corner radius to half the length of the side of
* the shape. When the adjacent corner is not rounded, we want to allow radii that are more than
* 50% of that side.
*/
private void drawBorderWithPath(Canvas canvas, Paint paint) {
Path path = new Path();
int height = initialHeight;
int width = initialWidth;
// This is necessary because of extra height/width added for borders.
if (!hasTopBorder) {
height += borderWidth;
}
if (!hasLeftBorder) {
width += borderWidth;
}
if (hasTopBorder) {
if (cornerRadii[0] != 0) {
// Add second half of top left corner.
float radius = cornerRadii[0];
path.addArc(topLeftBoundingBox(radius), 225, 45);
}
if (cornerRadii[2] != 0) {
// If the top right corner is rounded, add the top border and half the top right corner.
float radius = cornerRadii[2];
path.lineTo(width - radius, 0);
path.addArc(topRightBoundingBox(width, radius), 270, 46);
} else {
// Add border across top.
path.lineTo(width, 0);
}
} else {
// If there is no top border, jump to the top right corner.
path.moveTo(width, 0);
}
if (hasRightBorder) {
if (cornerRadii[2] != 0) {
// Add second half of top right corner.
float radius = cornerRadii[2];
path.addArc(topRightBoundingBox(width, radius), 315, 45);
}
if (cornerRadii[4] != 0) {
// If the bottom right corner is rounded, add right border and half the bottom right corner.
float radius = cornerRadii[4];
path.lineTo(width, height - radius);
path.addArc(bottomRightBoundingBox(width, height, radius), 0, 46);
} else {
// Add right border, no rounded corner at bottom.
path.lineTo(width, height);
}
} else {
// If there is no right border, jump to the bottom right corner.
path.moveTo(width, height);
}
if (hasBottomBorder) {
if (cornerRadii[4] != 0) {
// Add second half of bottom right corner.
float radius = cornerRadii[4];
path.addArc(bottomRightBoundingBox(width, height, radius), 45, 45);
}
if (cornerRadii[6] != 0) {
// Add bottom border with rounded corner at bottom left.
float radius = cornerRadii[6];
path.lineTo(radius, height);
path.addArc(bottomLeftBoundingBox(height, radius), 90, 46);
} else {
path.lineTo(0, height);
}
} else {
// If there is no bottom border, jump to the bottom left corner.
path.moveTo(0, height);
}
if (hasLeftBorder) {
if (cornerRadii[6] != 0) {
// Add second half of bottom left corner.
float radius = cornerRadii[6];
path.addArc(bottomLeftBoundingBox(height, radius), 135, 45);
}
if (cornerRadii[0] != 0) {
// Add left border with rounded corner at top left.
float radius = cornerRadii[0];
path.lineTo(0, radius);
path.addArc(topLeftBoundingBox(radius), 180, 46);
} else {
// Add left border.
path.lineTo(0, 0);
}
}
// Actually draw the path that was just built. The paint defines the width/color of the border.
canvas.drawPath(path, paint);
}
private RectF topLeftBoundingBox(float radius) {
return new RectF(0, 0, radius * 2, radius * 2);
}
private RectF topRightBoundingBox(float width, float radius) {
return new RectF(width - (radius * 2), 0, width, radius * 2);
}
private RectF bottomRightBoundingBox(float width, float height, float radius) {
return new RectF(width - (radius * 2), height - (radius * 2), width, height);
}
private RectF bottomLeftBoundingBox(float height, float radius) {
return new RectF(0, height - (radius * 2), radius * 2, height);
}
}