java.lang.Object
↳ViewGroup
↳androidx.wear.widget.ArcLayout
Gradle dependencies
compile group: 'androidx.wear', name: 'wear', version: '1.4.0-alpha01'
- groupId: androidx.wear
- artifactId: wear
- version: 1.4.0-alpha01
Artifact androidx.wear:wear:1.4.0-alpha01 it located at Google repository (https://maven.google.com/)
Androidx artifact mapping:
androidx.wear:wear com.android.support:wear
Overview
Container which will lay its elements out on an arc. Elements will be relative to a given
anchor angle (where 0 degrees = 12 o clock), where the layout relative to the anchor angle is
controlled using anchorAngleDegrees and anchorType. The thickness of the arc is
calculated based on the child element with the greatest height (in the case of Android
widgets), or greatest thickness (for curved widgets). By default, the container lays its
children one by one in clockwise direction. The attribute 'clockwise' can be set to false to
make the layout direction as anti-clockwise. These two types of widgets will be drawn as
follows.
Standard Android Widgets:
These widgets will be drawn as usual, but placed at the correct position on the arc, with
the correct amount of rotation applied. As an example, for an Android Text widget, the text
baseline would be drawn at a tangent to the arc. The arc length of a widget is obtained by
measuring the width of the widget, and transforming that to the length of an arc on a circle.
A standard Android widget will be measured as usual, but the maximum height constraint will be
capped at the minimum radius of the arc (i.e. width / 2).
"Curved" widgets:
Widgets which implement ArcLayout.Widget are expected to draw themselves within an arc
automatically. These widgets will be measured with the full dimensions of the arc container.
They are also expected to provide their thickness (used when calculating the thickness of the
arc) and the current sweep angle (used for laying out when drawing). Note that the
ArcLayout will apply a rotation transform to the canvas before drawing this child; the
inner child need not perform any rotations itself.
An example of a widget which implements this interface is CurvedTextView, which
will lay itself out along the arc.
Summary
Fields |
---|
public static final int | ANCHOR_CENTER Anchor at the center of the set of elements drawn within this container. |
public static final int | ANCHOR_END Anchor at the end of the set of elements drawn within this container. |
public static final int | ANCHOR_START Anchor at the start of the set of elements drawn within this container. |
Constructors |
---|
public | ArcLayout(Context context)
|
public | ArcLayout(Context context, AttributeSet attrs)
|
public | ArcLayout(Context context, AttributeSet attrs, int defStyleAttr)
|
public | ArcLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
|
Methods |
---|
protected boolean | checkLayoutParams(ViewGroup.LayoutParams p)
|
protected boolean | drawChild(Canvas canvas, View child, long drawingTime)
|
protected ViewGroup.LayoutParams | generateDefaultLayoutParams()
|
public ViewGroup.LayoutParams | generateLayoutParams(AttributeSet attrs)
|
protected ViewGroup.LayoutParams | generateLayoutParams(ViewGroup.LayoutParams p)
|
public float | getAnchorAngleDegrees()
Returns the anchor angle used for this container, in degrees. |
public int | getAnchorType()
Returns the anchor type used for this container. |
public float | getMaxAngleDegrees()
Returns the target angle that will be used by the layout when expanding child views with
weights (see ArcLayout.LayoutParams.setWeight(float)). |
public boolean | isClockwise()
returns the layout direction |
public boolean | onInterceptTouchEvent(MotionEvent event)
|
protected void | onLayout(boolean changed, int l, int t, int r, int b)
|
protected void | onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
public boolean | onTouchEvent(MotionEvent event)
|
public void | requestLayout()
|
public void | setAnchorAngleDegrees(float anchorAngleDegrees)
Sets the anchor angle used for this container, in degrees. |
public void | setAnchorType(int anchorType)
Sets the anchor type used for this container. |
public void | setClockwise(boolean clockwise)
Sets the layout direction |
public void | setMaxAngleDegrees(float maxAngleDegrees)
Sets the target angle that will be used by the layout when expanding child views with
weights (see ArcLayout.LayoutParams.setWeight(float)). |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
ANCHOR_STARTAnchor at the start of the set of elements drawn within this container. This causes the first
child to be drawn from anchorAngle degrees, to the right.
As an example, if this container contains two arcs, one having 10 degrees of sweep and the
other having 20 degrees of sweep, the first will be drawn between 0-10 degrees, and the
second between 10-30 degrees.
public static final int
ANCHOR_CENTERAnchor at the center of the set of elements drawn within this container.
As an example, if this container contains two arcs, one having 10 degrees of sweep and the
other having 20 degrees of sweep, the first will be drawn between -15 and -5 degrees, and the
second between -5 and 15 degrees.
public static final int
ANCHOR_ENDAnchor at the end of the set of elements drawn within this container. This causes the last
element to end at anchorAngle degrees, with the other elements swept to the left.
As an example, if this container contains two arcs, one having 10 degrees of sweep and the
other having 20 degrees of sweep, the first will be drawn between -30 and -20 degrees, and
the second between -20 and 0 degrees.
Constructors
public
ArcLayout(Context context)
public
ArcLayout(Context context, AttributeSet attrs)
public
ArcLayout(Context context, AttributeSet attrs, int defStyleAttr)
public
ArcLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
Methods
public void
requestLayout()
protected void
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void
onLayout(boolean changed, int l, int t, int r, int b)
public boolean
onInterceptTouchEvent(MotionEvent event)
public boolean
onTouchEvent(MotionEvent event)
protected boolean
drawChild(Canvas canvas, View child, long drawingTime)
protected boolean
checkLayoutParams(ViewGroup.LayoutParams p)
protected ViewGroup.LayoutParams
generateLayoutParams(ViewGroup.LayoutParams p)
public ViewGroup.LayoutParams
generateLayoutParams(AttributeSet attrs)
protected ViewGroup.LayoutParams
generateDefaultLayoutParams()
public int
getAnchorType()
Returns the anchor type used for this container.
public void
setAnchorType(int anchorType)
Sets the anchor type used for this container.
public float
getAnchorAngleDegrees()
Returns the anchor angle used for this container, in degrees.
public void
setAnchorAngleDegrees(float anchorAngleDegrees)
Sets the anchor angle used for this container, in degrees.
public float
getMaxAngleDegrees()
Returns the target angle that will be used by the layout when expanding child views with
weights (see ArcLayout.LayoutParams.setWeight(float)).
public void
setMaxAngleDegrees(float maxAngleDegrees)
Sets the target angle that will be used by the layout when expanding child views with
weights (see ArcLayout.LayoutParams.setWeight(float)). If not set the default is 360 degrees. This
target may not be achievable if other non-expandable views bring us past this value.
public boolean
isClockwise()
returns the layout direction
public void
setClockwise(boolean clockwise)
Sets the layout direction
Source
/*
* Copyright 2020 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 androidx.wear.widget;
import static java.lang.Math.asin;
import static java.lang.Math.max;
import static java.lang.Math.round;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.wear.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Container which will lay its elements out on an arc. Elements will be relative to a given
* anchor angle (where 0 degrees = 12 o clock), where the layout relative to the anchor angle is
* controlled using {@code anchorAngleDegrees} and {@code anchorType}. The thickness of the arc is
* calculated based on the child element with the greatest height (in the case of Android
* widgets), or greatest thickness (for curved widgets). By default, the container lays its
* children one by one in clockwise direction. The attribute 'clockwise' can be set to false to
* make the layout direction as anti-clockwise. These two types of widgets will be drawn as
* follows.
*
* <p>Standard Android Widgets:
*
* <p>These widgets will be drawn as usual, but placed at the correct position on the arc, with
* the correct amount of rotation applied. As an example, for an Android Text widget, the text
* baseline would be drawn at a tangent to the arc. The arc length of a widget is obtained by
* measuring the width of the widget, and transforming that to the length of an arc on a circle.
*
* <p>A standard Android widget will be measured as usual, but the maximum height constraint will be
* capped at the minimum radius of the arc (i.e. width / 2).
*
* <p>"Curved" widgets:
*
* <p>Widgets which implement {@link ArcLayout.Widget} are expected to draw themselves within an arc
* automatically. These widgets will be measured with the full dimensions of the arc container.
* They are also expected to provide their thickness (used when calculating the thickness of the
* arc) and the current sweep angle (used for laying out when drawing). Note that the
* ArcLayout will apply a rotation transform to the canvas before drawing this child; the
* inner child need not perform any rotations itself.
*
* <p>An example of a widget which implements this interface is {@link CurvedTextView}, which
* will lay itself out along the arc.
*/
@UiThread
public class ArcLayout extends ViewGroup {
/**
* Interface for a widget which knows it is being rendered inside an arc, and will draw
* itself accordingly. Any widget implementing this interface will receive the full-sized
* canvas, pre-rotated, in its draw call.
*/
public interface Widget {
/** Returns the sweep angle that this widget is drawn with. */
@FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
float getSweepAngleDegrees();
/**
* Set the sweep angle that this widget is drawn with. This is only called during layout,
* and only if the {@link LayoutParams#mWeight} is non-zero. Note your widget will need to
* handle alignment.
*/
default void setSweepAngleDegrees(
@FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) float sweepAngleDegrees) {
}
/** Returns the thickness of this widget inside the arc. */
@Px
int getThickness();
/**
* Check whether the widget contains invalid attributes as a child of ArcLayout, throwing
* a Exception if something is wrong.
* This is important for widgets that can be both standalone or used inside an ArcLayout,
* some parameters used when the widget is standalone doesn't make sense when the widget
* is inside an ArcLayout.
*/
void checkInvalidAttributeAsChild();
/**
* Return true when the given point is in the clickable area of the child widget.
* In particular, the coordinates should be considered as if the child was drawn
* centered at the default angle (12 o clock).
*/
boolean isPointInsideClickArea(float x, float y);
}
/**
* Layout parameters for a widget added to an arc. This allows each element to specify
* whether or not it should be rotated(around the center of the child) when drawn inside the
* arc. For example, when the child is put at the center-bottom of the arc, whether the
* parent layout is responsible to rotate it 180 degree to draw it upside down.
*
* <p>Note that the {@code rotate} parameter is ignored when drawing "Fullscreen" elements.
*/
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/** Vertical alignment of elements within the arc. */
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@IntDef({VERTICAL_ALIGN_OUTER, VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_INNER})
public @interface VerticalAlignment {
}
/** Align to the outer edge of the parent ArcLayout. */
public static final int VERTICAL_ALIGN_OUTER = 0;
/** Align to the center of the parent ArcLayout. */
public static final int VERTICAL_ALIGN_CENTER = 1;
/** Align to the inner edge of the parent ArcLayout. */
public static final int VERTICAL_ALIGN_INNER = 2;
private boolean mRotated = true;
@VerticalAlignment
private int mVerticalAlignment = VERTICAL_ALIGN_CENTER;
// Internally used during layout/draw
// Stores the angle of the child, used to handle touch events.
float mMiddleAngle;
// Position of the center of the child, in the parent's coordinate space.
// Currently only used for normal (not ArcLayout.Widget) children.
float mCenterX;
float mCenterY;
// The layout weight for this view, a value of zero means no expansion.
float mWeight;
/**
* Creates a new set of layout parameters. The values are extracted from the supplied
* attributes set and context.
*
* @param context The Context the ArcLayout is running in, through which it can access the
* current theme, resources, etc.
* @param attrs The set of attributes from which to extract the layout parameters' values
*/
public LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcLayout_Layout);
mRotated = a.getBoolean(R.styleable.ArcLayout_Layout_layout_rotate, true);
mVerticalAlignment =
a.getInt(R.styleable.ArcLayout_Layout_layout_valign, VERTICAL_ALIGN_CENTER);
mWeight = a.getFloat(R.styleable.ArcLayout_Layout_layout_weight, 0f);
a.recycle();
}
/**
* Creates a new set of layout parameters with specified width and height
*
* @param width The width, either WRAP_CONTENT, MATCH_PARENT or a fixed size in pixels
* @param height The height, either WRAP_CONTENT, MATCH_PARENT or a fixed size in pixels
*/
public LayoutParams(int width, int height) {
super(width, height);
}
/** Copy constructor */
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
/**
* Gets whether the widget shall be rotated by the ArcLayout container corresponding
* to its layout position angle
*/
public boolean isRotated() {
return mRotated;
}
/**
* Sets whether the widget shall be rotated by the ArcLayout container corresponding
* to its layout position angle
*/
public void setRotated(boolean rotated) {
mRotated = rotated;
}
/**
* Gets how the widget is positioned vertically in the ArcLayout.
*/
@VerticalAlignment
public int getVerticalAlignment() {
return mVerticalAlignment;
}
/**
* Sets how the widget is positioned vertically in the ArcLayout.
*
* @param verticalAlignment align the widget to outer, inner edges or center.
*/
public void setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
mVerticalAlignment = verticalAlignment;
}
/** Returns the weight used for computing expansion. */
public float getWeight() {
return mWeight;
}
/**
* Indicates how much of the extra space in the ArcLayout will be allocated to the
* view associated with these LayoutParams up to the limit specified by
* {@link ArcLayout#setMaxAngleDegrees}. Specify 0 if the view should not be
* stretched.
* Otherwise the extra pixels will be pro-rated among all views whose weight is greater than
* 0.
*
* Note non zero weights are only supported for Views that implement {@link ArcLayout
* .Widget}.
*/
public void setWeight(float weight) {
mWeight = weight;
}
}
/** Annotation for anchor types. */
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@IntDef({ANCHOR_START, ANCHOR_CENTER, ANCHOR_END})
public @interface AnchorType {
}
/**
* Anchor at the start of the set of elements drawn within this container. This causes the first
* child to be drawn from {@code anchorAngle} degrees, to the right.
*
* <p>As an example, if this container contains two arcs, one having 10 degrees of sweep and the
* other having 20 degrees of sweep, the first will be drawn between 0-10 degrees, and the
* second between 10-30 degrees.
*/
public static final int ANCHOR_START = 0;
/**
* Anchor at the center of the set of elements drawn within this container.
*
* <p>As an example, if this container contains two arcs, one having 10 degrees of sweep and the
* other having 20 degrees of sweep, the first will be drawn between -15 and -5 degrees, and the
* second between -5 and 15 degrees.
*/
public static final int ANCHOR_CENTER = 1;
/**
* Anchor at the end of the set of elements drawn within this container. This causes the last
* element to end at {@code anchorAngle} degrees, with the other elements swept to the left.
*
* <p>As an example, if this container contains two arcs, one having 10 degrees of sweep and the
* other having 20 degrees of sweep, the first will be drawn between -30 and -20 degrees, and
* the second between -20 and 0 degrees.
*/
public static final int ANCHOR_END = 2;
private static final float DEFAULT_START_ANGLE_DEGREES = 0f;
private static final boolean DEFAULT_LAYOUT_DIRECTION_IS_CLOCKWISE = true; // clockwise
@AnchorType
private static final int DEFAULT_ANCHOR_TYPE = ANCHOR_START;
private int mThicknessPx = 0;
@AnchorType
private int mAnchorType;
private float mAnchorAngleDegrees;
/**
* This is the target angle that will be used by the layout when expanding child views with
* weights.
*/
private float mMaxAngleDegrees = 360.0f;
private boolean mClockwise;
private final ChildArcAngles mChildArcAngles = new ChildArcAngles();
public ArcLayout(@NonNull Context context) {
this(context, null);
}
public ArcLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ArcLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ArcLayout(
@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a =
context.obtainStyledAttributes(
attrs, R.styleable.ArcLayout, defStyleAttr, defStyleRes
);
mAnchorType = a.getInt(R.styleable.ArcLayout_anchorPosition, DEFAULT_ANCHOR_TYPE);
mAnchorAngleDegrees =
a.getFloat(
R.styleable.ArcLayout_anchorAngleDegrees, DEFAULT_START_ANGLE_DEGREES
);
mClockwise = a.getBoolean(
R.styleable.ArcLayout_clockwise, DEFAULT_LAYOUT_DIRECTION_IS_CLOCKWISE
);
a.recycle();
}
@Override
public void requestLayout() {
super.requestLayout();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).forceLayout();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Need to derive the thickness of the curve from the children. We're a curve, so the
// children can only be sized up to (width or height)/2 units. This currently only
// supports fitting to a circle.
//
// No matter what, fit to the given size, be it a maximum or a fixed size. It doesn't make
// sense for this container to wrap its children.
int actualWidthPx = MeasureSpec.getSize(widthMeasureSpec);
int actualHeightPx = MeasureSpec.getSize(heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
// We can't actually resolve this.
// Let's fit to the screen dimensions, for need of anything better...
DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
actualWidthPx = displayMetrics.widthPixels;
actualHeightPx = displayMetrics.heightPixels;
}
// Fit to a square.
if (actualWidthPx < actualHeightPx) {
actualHeightPx = actualWidthPx;
} else if (actualHeightPx < actualWidthPx) {
actualWidthPx = actualHeightPx;
}
int maxChildDimension = actualHeightPx / 2;
// Measure all children in the new measurespec, and cache the largest.
int childMeasureSpec = MeasureSpec.makeMeasureSpec(maxChildDimension, MeasureSpec.AT_MOST);
// We need to do two measure passes. First, we need to measure all "normal" children, and
// get the thickness of all "CurvedContainer" children. Once we have that, we know the
// maximum thickness, and we can lay out the "CurvedContainer" children, taking into
// account their vertical alignment.
int maxChildHeightPx = 0;
int childState = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// ArcLayoutWidget is a special case. Because of how it draws, fit it to the size
// of the whole widget.
int childMeasuredHeight;
if (child instanceof Widget) {
childMeasuredHeight = ((Widget) child).getThickness();
} else {
measureChild(
child,
getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().width),
getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().height)
);
childMeasuredHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
}
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
maxChildHeightPx = max(maxChildHeightPx, childMeasuredHeight
+ childLayoutParams.topMargin + childLayoutParams.bottomMargin);
}
mThicknessPx = maxChildHeightPx;
// And now do the pass for the ArcLayoutWidgets
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
if (child instanceof Widget) {
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
float insetPx = getChildTopInset(child);
int innerChildMeasureSpec =
MeasureSpec.makeMeasureSpec(
maxChildDimension * 2 - round(insetPx * 2), MeasureSpec.EXACTLY);
measureChild(
child,
getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.width),
getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.height)
);
childState = combineMeasuredStates(childState, child.getMeasuredState());
}
}
setMeasuredDimension(
resolveSizeAndState(actualWidthPx, widthMeasureSpec, childState),
resolveSizeAndState(actualHeightPx, heightMeasureSpec, childState));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final boolean isLayoutRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
// != is equivalent to xor, we want to invert clockwise when the layout is rtl
final float multiplier = mClockwise != isLayoutRtl ? 1f : -1f;
// Layout the children in the arc, computing the center angle where they should be drawn.
float currentCumulativeAngle = calculateInitialRotation(multiplier);
// Compute the sum of any weights and the sum of the angles take up by fixed sized children.
// Unfortunately we can't move this to measure because calculateArcAngle relies upon
// getMeasuredWidth() which returns 0 in measure.
float totalAngle = 0f;
float weightSum = 0f;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
if (childLayoutParams.mWeight > 0) {
weightSum += childLayoutParams.mWeight;
calculateArcAngle(child, mChildArcAngles);
totalAngle +=
mChildArcAngles.leftMarginAsAngle + mChildArcAngles.rightMarginAsAngle;
} else {
calculateArcAngle(child, mChildArcAngles);
totalAngle += mChildArcAngles.getTotalAngle();
}
}
float weightMultiplier = 0f;
if (weightSum > 0f) {
weightMultiplier = (mMaxAngleDegrees - totalAngle) / weightSum;
}
// Now perform the layout.
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
calculateArcAngle(child, mChildArcAngles);
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
if (childLayoutParams.mWeight > 0) {
mChildArcAngles.actualChildAngle = childLayoutParams.mWeight * weightMultiplier;
if (child instanceof Widget) {
// NB we need to be careful since the child itself may set this value dueing
// measure.
((Widget) child).setSweepAngleDegrees(mChildArcAngles.actualChildAngle);
} else {
throw new IllegalStateException("ArcLayout.LayoutParams with non zero weights"
+ " are only supported for views implementing ArcLayout.Widget");
}
}
float preRotation = mChildArcAngles.leftMarginAsAngle
+ mChildArcAngles.actualChildAngle / 2f;
float middleAngle = multiplier * (currentCumulativeAngle + preRotation);
childLayoutParams.mMiddleAngle = middleAngle;
// Distance from the center of the ArcLayout to the center of the child widget
float centerToCenterDistance = (getMeasuredHeight() - child.getMeasuredHeight()) / 2
- getChildTopInset(child);
// Move the center of the widget in the circle centered on this ArcLayout, and with
// radius centerToCenterDistance
childLayoutParams.mCenterX =
(float) (getMeasuredWidth() / 2f
+ centerToCenterDistance * Math.sin(middleAngle * Math.PI / 180));
childLayoutParams.mCenterY =
(float) (getMeasuredHeight() / 2f
- centerToCenterDistance * Math.cos(middleAngle * Math.PI / 180));
currentCumulativeAngle += mChildArcAngles.getTotalAngle();
// Curved container widgets have been measured so that the "arc" inside their widget
// will touch the outside of the box they have been measured in, taking into account
// the vertical alignment. Just grow them from the center.
if (child instanceof Widget) {
int leftPx =
round((getMeasuredWidth() / 2f) - (child.getMeasuredWidth() / 2f));
int topPx =
round((getMeasuredHeight() / 2f) - (child.getMeasuredHeight() / 2f));
child.layout(
leftPx,
topPx,
leftPx + child.getMeasuredWidth(),
topPx + child.getMeasuredHeight()
);
} else {
// Normal widget's centers need to be placed on their final position,
// the only thing left for drawing is to maybe rotate them.
int leftPx = round(childLayoutParams.mCenterX - child.getMeasuredWidth() / 2f);
int topPx = round(childLayoutParams.mCenterY - child.getMeasuredHeight() / 2f);
child.layout(leftPx, topPx, leftPx + child.getMeasuredWidth(),
topPx + child.getMeasuredHeight());
}
}
}
// When a view (that can handle it) receives a TOUCH_DOWN event, it will get all subsequent
// events until the touch is released, even if the pointer goes outside of it's bounds.
private View mTouchedView = null;
@Override
public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
if (mTouchedView == null && event.getActionMasked() == MotionEvent.ACTION_DOWN) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// Ensure that the view is visible
if (child.getVisibility() != VISIBLE) {
continue;
}
// Map the event to the child's coordinate system
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
float angle = childLayoutParams.mMiddleAngle;
float[] point = new float[]{event.getX(), event.getY()};
mapPoint(child, angle, point);
// Check if the click is actually in the child area
float x = point[0];
float y = point[1];
if (insideChildClickArea(child, x, y)) {
mTouchedView = child;
break;
}
}
}
// We can't do normal dispatching because it will capture touch in the original position
// of children.
return true;
}
private static boolean insideChildClickArea(View child, float x, float y) {
if (child instanceof Widget) {
return ((Widget) child).isPointInsideClickArea(x, y);
}
return x >= 0 && x < child.getMeasuredWidth() && y >= 0 && y < child.getMeasuredHeight();
}
// Map a point to local child coordinates.
private void mapPoint(View child, float angle, float[] point) {
Matrix m = new Matrix();
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
if (child instanceof Widget) {
m.postRotate(-angle, getMeasuredWidth() / 2, getMeasuredHeight() / 2);
m.postTranslate(-child.getX(), -child.getY());
} else {
m.postTranslate(-childLayoutParams.mCenterX, -childLayoutParams.mCenterY);
if (childLayoutParams.isRotated()) {
m.postRotate(-angle);
}
m.postTranslate(child.getWidth() / 2, child.getHeight() / 2);
}
m.mapPoints(point);
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (mTouchedView != null) {
// Map the event's coordinates to the child's coordinate space
float[] point = new float[]{event.getX(), event.getY()};
LayoutParams touchedViewLayoutParams = (LayoutParams) mTouchedView.getLayoutParams();
mapPoint(mTouchedView, touchedViewLayoutParams.mMiddleAngle, point);
float dx = point[0] - event.getX();
float dy = point[1] - event.getY();
event.offsetLocation(dx, dy);
mTouchedView.dispatchTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_UP
|| event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
// We have finished handling these series of events.
mTouchedView = null;
}
return true;
}
return false;
}
@Override
protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
// Rotate the canvas to make the children render in the right place.
canvas.save();
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
float middleAngle = childLayoutParams.mMiddleAngle;
if (child instanceof Widget) {
// Rotate the child widget. This rotation places child widget in its correct place in
// the circle. Rotation is done around the center of the circle that components make.
canvas.rotate(
middleAngle,
getMeasuredWidth() / 2f,
getMeasuredHeight() / 2f);
((Widget) child).checkInvalidAttributeAsChild();
} else {
// Normal components already have their center in the right position during layout,
// the only thing remaining is any needed rotation.
// This rotation is done in place around the center of the
// child to adjust it based on rotation and clockwise attributes.
float angleToRotate = childLayoutParams.isRotated()
? middleAngle + (mClockwise ? 0f : 180f)
: 0f;
canvas.rotate(angleToRotate, childLayoutParams.mCenterX, childLayoutParams.mCenterY);
}
boolean wasInvalidateIssued = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return wasInvalidateIssued;
}
private float calculateInitialRotation(float multiplier) {
if (mAnchorType == ANCHOR_START) {
return multiplier * mAnchorAngleDegrees;
}
float totalArcAngle = 0;
boolean hasWeights = false;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
if (childLayoutParams.getWeight() > 0f) {
hasWeights = true;
}
calculateArcAngle(child, mChildArcAngles);
totalArcAngle += mChildArcAngles.getTotalAngle();
}
if (hasWeights && totalArcAngle < mMaxAngleDegrees) {
totalArcAngle = mMaxAngleDegrees;
}
if (mAnchorType == ANCHOR_CENTER) {
return multiplier * mAnchorAngleDegrees - (totalArcAngle / 2f);
} else if (mAnchorType == ANCHOR_END) {
return multiplier * mAnchorAngleDegrees - totalArcAngle;
}
return 0;
}
private static float widthToAngleDegrees(float widthPx, float radiusPx) {
return (float) Math.toDegrees(2 * asin(widthPx / radiusPx / 2f));
}
private void calculateArcAngle(@NonNull View view, @NonNull ChildArcAngles childAngles) {
if (view.getVisibility() == GONE) {
childAngles.leftMarginAsAngle = 0;
childAngles.rightMarginAsAngle = 0;
childAngles.actualChildAngle = 0;
return;
}
float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
LayoutParams childLayoutParams = (LayoutParams) view.getLayoutParams();
childAngles.leftMarginAsAngle =
widthToAngleDegrees(childLayoutParams.leftMargin, radiusPx);
childAngles.rightMarginAsAngle =
widthToAngleDegrees(childLayoutParams.rightMargin, radiusPx);
if (view instanceof Widget) {
childAngles.actualChildAngle = ((Widget) view).getSweepAngleDegrees();
} else {
childAngles.actualChildAngle =
widthToAngleDegrees(view.getMeasuredWidth(), radiusPx);
}
}
private float getChildTopInset(@NonNull View child) {
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
int childHeight = child instanceof Widget
? ((Widget) child).getThickness()
: child.getMeasuredHeight();
int thicknessDiffPx =
mThicknessPx - childLayoutParams.topMargin - childLayoutParams.bottomMargin
- childHeight;
int margin = mClockwise ? childLayoutParams.topMargin : childLayoutParams.bottomMargin;
float topInset = margin + getChildTopOffset(child);
switch (childLayoutParams.getVerticalAlignment()) {
case LayoutParams.VERTICAL_ALIGN_OUTER:
return topInset;
case LayoutParams.VERTICAL_ALIGN_CENTER:
return topInset + thicknessDiffPx / 2f;
case LayoutParams.VERTICAL_ALIGN_INNER:
return topInset + thicknessDiffPx;
default:
// Normally unreachable...
return 0;
}
}
/**
* For vertical rectangular screens, additional offset needs to be taken into the account for
* y position of normal widget in order to be in the correct place in the circle.
*/
private float getChildTopOffset(View child) {
if (child instanceof Widget || getMeasuredWidth() >= getMeasuredHeight()) {
return 0;
}
return round((getMeasuredHeight() - getMeasuredWidth()) / 2f);
}
@Override
protected boolean checkLayoutParams(@NonNull ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
@NonNull
protected ViewGroup.LayoutParams generateLayoutParams(@NonNull ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
@NonNull
public ViewGroup.LayoutParams generateLayoutParams(@NonNull AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
@NonNull
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
/** Returns the anchor type used for this container. */
@AnchorType
public int getAnchorType() {
return mAnchorType;
}
/** Sets the anchor type used for this container. */
public void setAnchorType(@AnchorType int anchorType) {
if (anchorType < ANCHOR_START || anchorType > ANCHOR_END) {
throw new IllegalArgumentException("Unknown anchor type");
}
mAnchorType = anchorType;
invalidate();
}
/** Returns the anchor angle used for this container, in degrees. */
@FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
public float getAnchorAngleDegrees() {
return mAnchorAngleDegrees;
}
/** Sets the anchor angle used for this container, in degrees. */
public void setAnchorAngleDegrees(
@FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) float anchorAngleDegrees) {
mAnchorAngleDegrees = anchorAngleDegrees;
invalidate();
}
/**
* Returns the target angle that will be used by the layout when expanding child views with
* weights (see {@link LayoutParams#setWeight}).
*/
@FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
public float getMaxAngleDegrees() {
return mMaxAngleDegrees;
}
/**
* Sets the target angle that will be used by the layout when expanding child views with
* weights (see {@link LayoutParams#setWeight}). If not set the default is 360 degrees. This
* target may not be achievable if other non-expandable views bring us past this value.
*/
public void setMaxAngleDegrees(
@FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
float maxAngleDegrees) {
mMaxAngleDegrees = maxAngleDegrees;
invalidate();
requestLayout();
}
/** returns the layout direction */
public boolean isClockwise() {
return mClockwise;
}
/** Sets the layout direction */
public void setClockwise(boolean clockwise) {
mClockwise = clockwise;
invalidate();
}
private static class ChildArcAngles {
public float leftMarginAsAngle;
public float rightMarginAsAngle;
public float actualChildAngle;
public float getTotalAngle() {
return leftMarginAsAngle + rightMarginAsAngle + actualChildAngle;
}
}
}