| /* |
| * 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. */ |
| /** @hide */ |
| @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. */ |
| /** @hide */ |
| @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; |
| |
| @SuppressWarnings("SyntheticAccessor") |
| 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; |
| } |
| } |
| } |