public class

ArcLayout

extends ViewGroup

 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 intANCHOR_CENTER

Anchor at the center of the set of elements drawn within this container.

public static final intANCHOR_END

Anchor at the end of the set of elements drawn within this container.

public static final intANCHOR_START

Anchor at the start of the set of elements drawn within this container.

Constructors
publicArcLayout(Context context)

publicArcLayout(Context context, AttributeSet attrs)

publicArcLayout(Context context, AttributeSet attrs, int defStyleAttr)

publicArcLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)

Methods
protected booleancheckLayoutParams(ViewGroup.LayoutParams p)

protected booleandrawChild(Canvas canvas, View child, long drawingTime)

protected ViewGroup.LayoutParamsgenerateDefaultLayoutParams()

public ViewGroup.LayoutParamsgenerateLayoutParams(AttributeSet attrs)

protected ViewGroup.LayoutParamsgenerateLayoutParams(ViewGroup.LayoutParams p)

public floatgetAnchorAngleDegrees()

Returns the anchor angle used for this container, in degrees.

public intgetAnchorType()

Returns the anchor type used for this container.

public floatgetMaxAngleDegrees()

Returns the target angle that will be used by the layout when expanding child views with weights (see ArcLayout.LayoutParams.setWeight(float)).

public booleanisClockwise()

returns the layout direction

public booleanonInterceptTouchEvent(MotionEvent event)

protected voidonLayout(boolean changed, int l, int t, int r, int b)

protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

public booleanonTouchEvent(MotionEvent event)

public voidrequestLayout()

public voidsetAnchorAngleDegrees(float anchorAngleDegrees)

Sets the anchor angle used for this container, in degrees.

public voidsetAnchorType(int anchorType)

Sets the anchor type used for this container.

public voidsetClockwise(boolean clockwise)

Sets the layout direction

public voidsetMaxAngleDegrees(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.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int ANCHOR_START

Anchor 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_CENTER

Anchor 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_END

Anchor 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;
        }
    }
}