blob: d90cedd702de889cd80c0646490b9050896689d1 [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 static com.google.android.libraries.feed.common.Validators.checkState;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.shapes.RoundRectShape;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewParent;
import android.widget.FrameLayout;
import com.google.android.libraries.feed.common.functional.Supplier;
import com.google.android.libraries.feed.common.ui.LayoutUtils;
import com.google.android.libraries.feed.piet.ui.RoundedCornerDelegateFactory.RoundingStrategy;
import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners.RadiusOptionsCase;
import com.google.search.now.ui.piet.StylesProto.Borders;
/**
* Wrapper for {@link View} instances in Piet that require rounded corners.
*
* <p>There are three strategies used to round corners in different situations. This class decides
* which to use based on the following rules:
*
* <p>OUTLINE_PROVIDER: This is always used when possible (unless disallowed by the host). It
* requires hardware acceleration, does not work if 1-3 corners are rounded, and does not work on
* K-.
*
* <p>CLIP_PATH: This works in all situations, but does not support anti-aliasing. Used when the
* device does not support hardware acceleration, and in K- where outline provider is not supported.
* Although BITMAP_MASKING would work for those cases, this strategy is much more performant. Older
* SDKs or no hw acceleration support typically are signs of less-performant phones. The lack of
* anti-aliasing does not negatively effect metrics for these users. This strategy can be disallowed
* by the host, in which case BITMAP_MASKING will be used where OUTLINE_PROVIDER is not possible.
*
* <p>BITMAP_MASKING: This implementation should work in all cases for all SDKs, but has the worst
* performance in CPU and memory usage. It is only used when 1-3 corners are rounded in L+, or if
* the other strategies are disallowed by the host or not possible.
*
* <p>A {@link RoundedCornerDelegate} is called in places where special logic is needed for
* different rounding strategies. Each strategy has its own delegate, which is created when the
* strategy is selected. This class doesn't know about the implementation details of the rounding
* strategies.
*/
public class RoundedCornerWrapperView extends FrameLayout {
private final int radiusOverride;
private final RoundedCorners roundedCorners;
private final Supplier<Boolean> isRtLSupplier;
private final Context context;
private final Borders borders;
private final boolean allowClipPath;
private final boolean allowOutlineRounding;
private final boolean hasRoundedCorners;
private final boolean allFourCornersRounded;
private final RoundedCornerMaskCache maskCache;
private RoundedCornerDelegate roundingDelegate;
private boolean drawSuperCalled;
// Keep track of current mask configuration so we can use cached values if nothing has changed.
int lastWidth = -1;
int lastHeight = -1;
private int roundedCornerRadius;
// Doesn't like the call to setOutlineProvider
@SuppressWarnings("initialization")
public RoundedCornerWrapperView(
Context context,
RoundedCorners roundedCorners,
RoundedCornerMaskCache maskCache,
Supplier<Boolean> isRtLSupplier,
int radiusOverride,
Borders borders,
boolean allowClipPath,
boolean allowOutlineRounding) {
super(context);
this.radiusOverride = radiusOverride;
this.roundedCorners = roundedCorners;
this.isRtLSupplier = isRtLSupplier;
this.allowClipPath = allowClipPath;
this.allowOutlineRounding = allowOutlineRounding;
this.context = context;
this.borders = borders;
this.maskCache = maskCache;
this.hasRoundedCorners =
RoundedCornerViewHelper.hasValidRoundedCorners(roundedCorners, radiusOverride);
this.allFourCornersRounded =
RoundedCornerViewHelper.allFourCornersRounded(roundedCorners.getBitmask());
setRoundingStrategy();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Even when not using the outline provider strategy, the outline needs to be set for
// shadows to render properly.
setupOutlineProvider();
}
setWillNotDraw(false);
}
private void setRoundingStrategy() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && allowClipPath) {
setRoundingDelegate(RoundingStrategy.CLIP_PATH);
if (VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN_MR2) {
// clipPath doesn't work with hardware rendering on < 18.
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
} else if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
&& allFourCornersRounded
&& allowOutlineRounding) {
setRoundingDelegate(RoundingStrategy.OUTLINE_PROVIDER);
} else {
setRoundingDelegate(RoundingStrategy.BITMAP_MASKING);
}
}
private void updateRoundingStrategy() {
if (roundingDelegate instanceof OutlineProviderRoundedCornerDelegate
&& this.isAttachedToWindow()
&& !this.isHardwareAccelerated()) {
if (allowClipPath) {
setRoundingDelegate(RoundingStrategy.CLIP_PATH);
} else {
setRoundingDelegate(RoundingStrategy.BITMAP_MASKING);
}
}
}
void setRoundingDelegate(RoundingStrategy strategy) {
if (roundingDelegate != null) {
roundingDelegate.destroy(this);
}
roundingDelegate =
RoundedCornerDelegateFactory.getDelegate(
strategy, maskCache, roundedCorners.getBitmask(), isRtLSupplier.get());
roundingDelegate.initializeForView(this);
}
private void setupOutlineProvider() {
if (hasRoundedCorners) {
super.setOutlineProvider(
new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
int radius = getRadius(view.getWidth(), view.getHeight());
if (allFourCornersRounded) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
return;
}
float[] radii =
RoundedCornerViewHelper.createRoundedCornerBitMask(
radius, roundedCorners.getBitmask(), isRtLSupplier.get());
RoundRectShape outlineShape = new RoundRectShape(radii, null, null);
outlineShape.resize(view.getWidth(), view.getHeight());
// This actually sets the outline to use this shape
outlineShape.getOutline(outline);
}
});
} else {
super.setOutlineProvider(ViewOutlineProvider.BOUNDS);
}
}
/**
* Calls the rounding delegate to perform any additional work specific to a certain rounding
* strategy during dispatchDraw().
*/
@Override
protected void dispatchDraw(Canvas canvas) {
roundingDelegate.dispatchDraw(canvas);
super.dispatchDraw(canvas);
}
/**
* Allows the {@link RoundedCornerDelegate} to add logic during onDescendantInvalidated().
*
* <p>The bitmap masking rounded corner strategy requires this hook, to make sure the wrapper view
* is invalidated when there are animations. The delegate handles that logic.
*/
@Override
public void onDescendantInvalidated(View child, View target) {
super.onDescendantInvalidated(child, target);
roundingDelegate.onDescendantInvalidated(this, target);
}
/**
* Allows the {@link RoundedCornerDelegate} to add logic during invalidateChildInParent().
*
* <p>The bitmap masking rounded corner strategy requires this hook, to make sure the wrapper view
* is invalidated when there are animations. The delegate handles that logic.
*/
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
roundingDelegate.invalidateChildInParent(this, dirty);
return super.invalidateChildInParent(location, dirty);
}
/**
* Calls the {@link RoundedCornerDelegate} to draw to the {@link Canvas}, in case extra logic is
* needed to round the corners.
*
* <p>Draws as normal for everything except the bitmap masking strategy. The bitmap masking
* strategy requires manipulating the {@link Canvas}, which the delegate handles.
*/
@Override
public void draw(Canvas canvas) {
drawSuperCalled = false;
roundingDelegate.draw(this, canvas);
checkState(drawSuperCalled, "View.draw() never called in RoundedCornerWrapperView.draw()");
}
public void drawSuper(Canvas canvas) {
drawSuperCalled = true;
super.draw(canvas);
}
/**
* Updates rounded corner information when the size changes.
*
* <p>At this point, the view will actually be attached to the window, meaning we can detect
* whether it is hardware accelerated. The outline provider strategy doesn't work without hardware
* acceleration, so the rounding strategy may need to be updated. The size always changes at least
* once--from 0 to final dimensions--so it is safe to assume that this will catch devices without
* hw acceleration.
*
* <p>Call the rounding delegate to perform any additional work specific to a certain rounding
* strategy.
*/
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
updateRoundingStrategy();
roundingDelegate.onSizeChanged(
getRadius(width, height), width, height, roundedCorners.getBitmask(), isRtLSupplier.get());
}
/**
* Lays out the view, calling the rounding delegate to perform any additional work specific to a
* certain rounding strategy.
*
* <p>Borders must be created after the radius is known, so they are added from onLayout().
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed || !hasRoundedCorners) {
return;
}
int width = getWidth();
int height = getHeight();
if (width == 0 || height == 0) {
// The view is not visible; no further processing is needed.
return;
}
int radius = getRadius(width, height);
addBorders(radius);
roundingDelegate.onLayout(radius, isRtLSupplier.get(), width, height);
}
/**
* Creates a border drawable and adds it to this view's foreground.
*
* <p>This is called from {@link RoundedCornerWrapperView} because when the corners are rounded,
* the borders also need to be rounded, and the radius can't properly be calculated until the view
* has been laid out and the height and width are known. From this class, it is easy to make sure
* the border is created after layout happens, since we are overriding draw. Draw is guaranteed to
* happen after layout, so we can make sure that the borders are drawn with the appropriate
* measurements.
*/
void addBorders(int radius) {
if (borders.getWidth() <= 0) {
return;
}
boolean isRtL = isRtLSupplier.get();
// Set up outline of borders
float[] outerRadii =
RoundedCornerViewHelper.createRoundedCornerBitMask(
radius, roundedCorners.getBitmask(), isRtL);
// Create a drawable to stroke the border
BorderDrawable borderDrawable =
new BorderDrawable(context, borders, outerRadii, isRtL, lastWidth, lastHeight);
this.setForeground(borderDrawable);
}
/**
* Returns the radius, which is only calculated if necessary.
*
* <p>If the width and height are the same as the previous width and height, there's no need to
* re-calculate, and the previous radius is returned. The width and height are needed for radii
* calculated as a percentage of width or height, and to make sure that the radius isn't too big
* for the size of the view.
*/
public int getRadius(int width, int height) {
if (!hasRoundedCorners || width == 0 || height == 0) {
return 0;
}
if (radiusOverride > 0) {
roundedCornerRadius = radiusOverride;
return roundedCornerRadius;
}
if (width == lastWidth && height == lastHeight) {
return roundedCornerRadius;
}
int radius = makeRadius(width, height);
lastWidth = width;
lastHeight = height;
return radius;
}
/** Calculates the corner radius, clipping to width or height when necessary. */
private int makeRadius(int width, int height) {
int radius = 0;
RadiusOptionsCase radiusOptions = roundedCorners.getRadiusOptionsCase();
switch (radiusOptions) {
case RADIUS_DP:
radius = (int) LayoutUtils.dpToPx(roundedCorners.getRadiusDp(), context);
break;
case RADIUS_PERCENTAGE_OF_HEIGHT:
radius = roundedCorners.getRadiusPercentageOfHeight() * height / 100;
break;
case RADIUS_PERCENTAGE_OF_WIDTH:
radius = roundedCorners.getRadiusPercentageOfWidth() * width / 100;
break;
default:
// TODO Remove deprecated radius code.
if (roundedCorners.hasRadius()) {
radius = (int) LayoutUtils.dpToPx(roundedCorners.getRadius(), context);
}
}
roundedCornerRadius =
RoundedCornerViewHelper.adjustRadiusIfTooBig(width, height, radius, roundedCorners);
return roundedCornerRadius;
}
}