2021-04-08 20:42:46 -04:00

262 lines
9.7 KiB
Java

/*
* Copyright 2017 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.
*/
package com.google.android.material.bottomappbar;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static java.lang.Math.abs;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.shape.EdgeTreatment;
import com.google.android.material.shape.ShapePath;
/**
* Top edge treatment for the bottom app bar which "cradles" a circular {@link
* FloatingActionButton}.
*
* <p>This edge features a downward semi-circular cutout from the edge line. The two corners created
* by the cutout can optionally be rounded. The circular cutout can also support a vertically offset
* FloatingActionButton; i.e., the cut-out need not be a perfect semi-circle, but could be an arc of
* less than 180 degrees that does not start or finish with a vertical path. This vertical offset
* must be positive.
*/
public class BottomAppBarTopEdgeTreatment extends EdgeTreatment implements Cloneable {
private static final int ARC_QUARTER = 90;
private static final int ARC_HALF = 180;
private static final int ANGLE_UP = 270;
private static final int ANGLE_LEFT = 180;
private static final float ROUNDED_CORNER_FAB_OFFSET = 1.75f;
private float roundedCornerRadius;
private float fabMargin;
private float fabDiameter;
private float cradleVerticalOffset;
private float horizontalOffset;
private float fabCornerSize = -1f;
/**
* @param fabMargin the margin in pixels between the cutout and the fab.
* @param roundedCornerRadius the radius, in pixels, of the rounded corners created by the cutout.
* A value of 0 will produce a sharp cutout.
* @param cradleVerticalOffset vertical offset, in pixels, of the {@link FloatingActionButton}
* being cradled. An offset of 0 indicates the vertical center of the {@link
* FloatingActionButton} is positioned on the top edge.
*/
public BottomAppBarTopEdgeTreatment(
float fabMargin, float roundedCornerRadius, float cradleVerticalOffset) {
this.fabMargin = fabMargin;
this.roundedCornerRadius = roundedCornerRadius;
setCradleVerticalOffset(cradleVerticalOffset);
this.horizontalOffset = 0f;
}
@Override
public void getEdgePath(
float length, float center, float interpolation, @NonNull ShapePath shapePath) {
if (fabDiameter == 0) {
// There is no cutout to draw.
shapePath.lineTo(length, 0);
return;
}
float cradleDiameter = fabMargin * 2 + fabDiameter;
float cradleRadius = cradleDiameter / 2f;
float roundedCornerOffset = interpolation * roundedCornerRadius;
float middle = center + horizontalOffset;
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
float verticalOffset =
interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius;
float verticalOffsetRatio = verticalOffset / cradleRadius;
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0);
return; // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
float cornerSize = fabCornerSize * interpolation;
boolean useCircleCutout = fabCornerSize == -1 || abs(fabCornerSize * 2f - fabDiameter) < .1f;
float arcOffset = 0;
if (!useCircleCutout) {
verticalOffset = 0;
arcOffset = ROUNDED_CORNER_FAB_OFFSET;
}
float distanceBetweenCenters = cradleRadius + roundedCornerOffset;
float distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters;
float distanceY = verticalOffset + roundedCornerOffset;
float distanceX = (float) Math.sqrt(distanceBetweenCentersSquared - (distanceY * distanceY));
// Calculate the x position of the rounded corner circles.
float leftRoundedCornerCircleX = middle - distanceX;
float rightRoundedCornerCircleX = middle + distanceX;
// Calculate the arc between the center of the two circles.
float cornerRadiusArcLength = (float) Math.toDegrees(Math.atan(distanceX / distanceY));
float cutoutArcOffset = (ARC_QUARTER - cornerRadiusArcLength) + arcOffset;
// Draw the starting line up to the left rounded corner.
shapePath.lineTo(/* x= */ leftRoundedCornerCircleX, /* y= */ 0);
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ leftRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ leftRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP,
/* sweepAngle= */ cornerRadiusArcLength);
if (useCircleCutout) {
// Draw the cutout circle.
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -cradleRadius - verticalOffset,
/* right= */ middle + cradleRadius,
/* bottom= */ cradleRadius - verticalOffset,
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ cutoutArcOffset * 2 - ARC_HALF);
} else {
float cutoutDiameter = fabMargin + cornerSize * 2f;
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle - cradleRadius + cutoutDiameter,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ (cutoutArcOffset * 2 - ARC_HALF) / 2f);
shapePath.lineTo(middle + cradleRadius - (cornerSize + fabMargin / 2f), /* y= */
(cornerSize + fabMargin));
shapePath.addArc(
/* left= */ middle + cradleRadius - (cornerSize * 2f + fabMargin),
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle + cradleRadius,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ 90,
/* sweepAngle= */ -90 + cutoutArcOffset);
}
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ rightRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ rightRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP - cornerRadiusArcLength,
/* sweepAngle= */ cornerRadiusArcLength);
// Draw the ending line after the right rounded corner.
shapePath.lineTo(/* x= */ length, /* y= */ 0);
}
/**
* Returns current fab diameter in pixels.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public float getFabDiameter() {
return fabDiameter;
}
/**
* Sets the fab diameter the size of the fab in pixels.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setFabDiameter(float fabDiameter) {
this.fabDiameter = fabDiameter;
}
/** Sets the horizontal offset, in pixels, of the cradle from center. */
void setHorizontalOffset(float horizontalOffset) {
this.horizontalOffset = horizontalOffset;
}
/**
* Returns the horizontal offset, in pixels, of the cradle from center.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public float getHorizontalOffset() {
return horizontalOffset;
}
/**
* Returns vertical offset, in pixels, of the {@link FloatingActionButton} being cradled. An
* offset of 0 indicates the vertical center of the {@link FloatingActionButton} is positioned on
* the top edge.
*/
float getCradleVerticalOffset() {
return cradleVerticalOffset;
}
/**
* Sets the vertical offset, in pixels, of the {@link FloatingActionButton} being cradled. An
* offset of 0 indicates the vertical center of the {@link FloatingActionButton} is positioned on
* the top edge.
*/
void setCradleVerticalOffset(@FloatRange(from = 0f) float cradleVerticalOffset) {
if (cradleVerticalOffset < 0) {
throw new IllegalArgumentException("cradleVerticalOffset must be positive.");
}
this.cradleVerticalOffset = cradleVerticalOffset;
}
float getFabCradleMargin() {
return fabMargin;
}
void setFabCradleMargin(float fabMargin) {
this.fabMargin = fabMargin;
}
float getFabCradleRoundedCornerRadius() {
return roundedCornerRadius;
}
void setFabCradleRoundedCornerRadius(float roundedCornerRadius) {
this.roundedCornerRadius = roundedCornerRadius;
}
public float getFabCornerRadius() {
return fabCornerSize;
}
public void setFabCornerSize(float size) {
this.fabCornerSize = size;
}
}