[go: nahoru, domu]

blob: 4c7c768965401ec192c0297f38a1544165646879 [file] [log] [blame]
Xiangyin Made5091d2020-10-26 20:08:08 +00001/*
2 * Copyright 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.wear.widget;
18
19import static java.lang.Math.asin;
20import static java.lang.Math.max;
21import static java.lang.Math.round;
22
Sergio Sancho0863d9c2020-11-17 11:28:45 +000023import android.annotation.SuppressLint;
Xiangyin Made5091d2020-10-26 20:08:08 +000024import android.content.Context;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
Sergio Sancho0863d9c2020-11-17 11:28:45 +000027import android.graphics.Matrix;
Xiangyin Made5091d2020-10-26 20:08:08 +000028import android.util.AttributeSet;
29import android.util.DisplayMetrics;
Sergio Sancho0863d9c2020-11-17 11:28:45 +000030import android.view.MotionEvent;
Xiangyin Made5091d2020-10-26 20:08:08 +000031import android.view.View;
32import android.view.ViewGroup;
33
Sergio Sancho25fc17f2021-04-27 18:03:58 +010034import androidx.annotation.FloatRange;
Xiangyin Made5091d2020-10-26 20:08:08 +000035import androidx.annotation.IntDef;
36import androidx.annotation.NonNull;
37import androidx.annotation.Nullable;
Sergio Sancho25fc17f2021-04-27 18:03:58 +010038import androidx.annotation.Px;
Xiangyin Made5091d2020-10-26 20:08:08 +000039import androidx.annotation.RestrictTo;
40import androidx.annotation.UiThread;
41import androidx.wear.R;
42
43import java.lang.annotation.Retention;
44import java.lang.annotation.RetentionPolicy;
45
46
47/**
48 * Container which will lay its elements out on an arc. Elements will be relative to a given
49 * anchor angle (where 0 degrees = 12 o clock), where the layout relative to the anchor angle is
50 * controlled using {@code anchorAngleDegrees} and {@code anchorType}. The thickness of the arc is
51 * calculated based on the child element with the greatest height (in the case of Android
52 * widgets), or greatest thickness (for curved widgets). By default, the container lays its
53 * children one by one in clockwise direction. The attribute 'clockwise' can be set to false to
54 * make the layout direction as anti-clockwise. These two types of widgets will be drawn as
55 * follows.
56 *
57 * <p>Standard Android Widgets:
58 *
59 * <p>These widgets will be drawn as usual, but placed at the correct position on the arc, with
60 * the correct amount of rotation applied. As an example, for an Android Text widget, the text
61 * baseline would be drawn at a tangent to the arc. The arc length of a widget is obtained by
62 * measuring the width of the widget, and transforming that to the length of an arc on a circle.
63 *
64 * <p>A standard Android widget will be measured as usual, but the maximum height constraint will be
65 * capped at the minimum radius of the arc (i.e. width / 2).
66 *
67 * <p>"Curved" widgets:
68 *
Sergio Sancho25fc17f2021-04-27 18:03:58 +010069 * <p>Widgets which implement {@link ArcLayout.Widget} are expected to draw themselves within an arc
Xiangyin Made5091d2020-10-26 20:08:08 +000070 * automatically. These widgets will be measured with the full dimensions of the arc container.
71 * They are also expected to provide their thickness (used when calculating the thickness of the
72 * arc) and the current sweep angle (used for laying out when drawing). Note that the
Sergio Sancho25fc17f2021-04-27 18:03:58 +010073 * ArcLayout will apply a rotation transform to the canvas before drawing this child; the
Xiangyin Made5091d2020-10-26 20:08:08 +000074 * inner child need not perform any rotations itself.
75 *
Sergio Sancho25fc17f2021-04-27 18:03:58 +010076 * <p>An example of a widget which implements this interface is {@link CurvedTextView}, which
Xiangyin Made5091d2020-10-26 20:08:08 +000077 * will lay itself out along the arc.
78 */
79@UiThread
Sergio Sancho25fc17f2021-04-27 18:03:58 +010080public class ArcLayout extends ViewGroup {
Xiangyin Made5091d2020-10-26 20:08:08 +000081
82 /**
83 * Interface for a widget which knows it is being rendered inside an arc, and will draw
84 * itself accordingly. Any widget implementing this interface will receive the full-sized
85 * canvas, pre-rotated, in its draw call.
86 */
Sergio Sancho25fc17f2021-04-27 18:03:58 +010087 public interface Widget {
Xiangyin Made5091d2020-10-26 20:08:08 +000088
89 /** Returns the sweep angle that this widget is drawn with. */
Sergio Sancho25fc17f2021-04-27 18:03:58 +010090 @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
Xiangyin Made5091d2020-10-26 20:08:08 +000091 float getSweepAngleDegrees();
92
Alex Clarke975ffbd2022-07-05 10:14:48 +010093 /**
94 * Set the sweep angle that this widget is drawn with. This is only called during layout,
95 * and only if the {@link LayoutParams#mWeight} is non-zero. Note your widget will need to
96 * handle alignment.
97 */
98 default void setSweepAngleDegrees(
99 @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) float sweepAngleDegrees) {
100 }
101
Xiangyin Made5091d2020-10-26 20:08:08 +0000102 /** Returns the thickness of this widget inside the arc. */
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100103 @Px
104 int getThickness();
Xiangyin Made5091d2020-10-26 20:08:08 +0000105
106 /**
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100107 * Check whether the widget contains invalid attributes as a child of ArcLayout, throwing
108 * a Exception if something is wrong.
109 * This is important for widgets that can be both standalone or used inside an ArcLayout,
110 * some parameters used when the widget is standalone doesn't make sense when the widget
111 * is inside an ArcLayout.
Xiangyin Made5091d2020-10-26 20:08:08 +0000112 */
Sergio Sancho27989312020-11-24 17:14:07 +0000113 void checkInvalidAttributeAsChild();
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000114
115 /**
116 * Return true when the given point is in the clickable area of the child widget.
117 * In particular, the coordinates should be considered as if the child was drawn
118 * centered at the default angle (12 o clock).
119 */
Sergio Sancho80596632021-04-22 13:22:02 +0100120 boolean isPointInsideClickArea(float x, float y);
Xiangyin Made5091d2020-10-26 20:08:08 +0000121 }
122
123 /**
124 * Layout parameters for a widget added to an arc. This allows each element to specify
125 * whether or not it should be rotated(around the center of the child) when drawn inside the
126 * arc. For example, when the child is put at the center-bottom of the arc, whether the
127 * parent layout is responsible to rotate it 180 degree to draw it upside down.
128 *
129 * <p>Note that the {@code rotate} parameter is ignored when drawing "Fullscreen" elements.
130 */
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000131 public static class LayoutParams extends ViewGroup.MarginLayoutParams {
Xiangyin Made5091d2020-10-26 20:08:08 +0000132
133 /** Vertical alignment of elements within the arc. */
134 /** @hide */
135 @Retention(RetentionPolicy.SOURCE)
136 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100137 @IntDef({VERTICAL_ALIGN_OUTER, VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_INNER})
Xiangyin Made5091d2020-10-26 20:08:08 +0000138 public @interface VerticalAlignment {
139 }
140
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100141 /** Align to the outer edge of the parent ArcLayout. */
142 public static final int VERTICAL_ALIGN_OUTER = 0;
Xiangyin Made5091d2020-10-26 20:08:08 +0000143
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100144 /** Align to the center of the parent ArcLayout. */
145 public static final int VERTICAL_ALIGN_CENTER = 1;
Xiangyin Made5091d2020-10-26 20:08:08 +0000146
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100147 /** Align to the inner edge of the parent ArcLayout. */
148 public static final int VERTICAL_ALIGN_INNER = 2;
Xiangyin Made5091d2020-10-26 20:08:08 +0000149
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100150 private boolean mRotated = true;
Xiangyin Made5091d2020-10-26 20:08:08 +0000151 @VerticalAlignment
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100152 private int mVerticalAlignment = VERTICAL_ALIGN_CENTER;
Xiangyin Made5091d2020-10-26 20:08:08 +0000153
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000154 // Internally used during layout/draw
155 // Stores the angle of the child, used to handle touch events.
156 float mMiddleAngle;
157
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100158 // Position of the center of the child, in the parent's coordinate space.
159 // Currently only used for normal (not ArcLayout.Widget) children.
160 float mCenterX;
161 float mCenterY;
162
Alex Clarke975ffbd2022-07-05 10:14:48 +0100163 // The layout weight for this view, a value of zero means no expansion.
164 float mWeight;
165
Xiangyin Made5091d2020-10-26 20:08:08 +0000166 /**
167 * Creates a new set of layout parameters. The values are extracted from the supplied
168 * attributes set and context.
169 *
Alex Clarke975ffbd2022-07-05 10:14:48 +0100170 * @param context The Context the ArcLayout is running in, through which it can access the
171 * current theme, resources, etc.
172 * @param attrs The set of attributes from which to extract the layout parameters' values
Xiangyin Made5091d2020-10-26 20:08:08 +0000173 */
174 public LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
175 super(context, attrs);
176
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100177 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcLayout_Layout);
Xiangyin Made5091d2020-10-26 20:08:08 +0000178
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100179 mRotated = a.getBoolean(R.styleable.ArcLayout_Layout_layout_rotate, true);
Xiangyin Made5091d2020-10-26 20:08:08 +0000180 mVerticalAlignment =
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100181 a.getInt(R.styleable.ArcLayout_Layout_layout_valign, VERTICAL_ALIGN_CENTER);
Alex Clarke975ffbd2022-07-05 10:14:48 +0100182 mWeight = a.getFloat(R.styleable.ArcLayout_Layout_layout_weight, 0f);
Xiangyin Made5091d2020-10-26 20:08:08 +0000183
184 a.recycle();
185 }
186
187 /**
188 * Creates a new set of layout parameters with specified width and height
189 *
Alex Clarke975ffbd2022-07-05 10:14:48 +0100190 * @param width The width, either WRAP_CONTENT, MATCH_PARENT or a fixed size in pixels
191 * @param height The height, either WRAP_CONTENT, MATCH_PARENT or a fixed size in pixels
Xiangyin Made5091d2020-10-26 20:08:08 +0000192 */
193 public LayoutParams(int width, int height) {
194 super(width, height);
195 }
196
197 /** Copy constructor */
198 public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
199 super(source);
200 }
201
202 /**
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100203 * Gets whether the widget shall be rotated by the ArcLayout container corresponding
Xiangyin Made5091d2020-10-26 20:08:08 +0000204 * to its layout position angle
205 */
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100206 public boolean isRotated() {
207 return mRotated;
Xiangyin Made5091d2020-10-26 20:08:08 +0000208 }
209
210 /**
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100211 * Sets whether the widget shall be rotated by the ArcLayout container corresponding
Xiangyin Made5091d2020-10-26 20:08:08 +0000212 * to its layout position angle
213 */
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100214 public void setRotated(boolean rotated) {
215 mRotated = rotated;
Xiangyin Made5091d2020-10-26 20:08:08 +0000216 }
217
218 /**
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100219 * Gets how the widget is positioned vertically in the ArcLayout.
Xiangyin Made5091d2020-10-26 20:08:08 +0000220 */
221 @VerticalAlignment
222 public int getVerticalAlignment() {
223 return mVerticalAlignment;
224 }
225
226 /**
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100227 * Sets how the widget is positioned vertically in the ArcLayout.
Alex Clarke975ffbd2022-07-05 10:14:48 +0100228 *
Xiangyin Made5091d2020-10-26 20:08:08 +0000229 * @param verticalAlignment align the widget to outer, inner edges or center.
230 */
231 public void setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
232 mVerticalAlignment = verticalAlignment;
233 }
Alex Clarke975ffbd2022-07-05 10:14:48 +0100234
235 /** Returns the weight used for computing expansion. */
236 public float getWeight() {
237 return mWeight;
238 }
239
240 /**
241 * Indicates how much of the extra space in the ArcLayout will be allocated to the
242 * view associated with these LayoutParams up to the limit specified by
243 * {@link ArcLayout#setMaxAngleDegrees}. Specify 0 if the view should not be
244 * stretched.
245 * Otherwise the extra pixels will be pro-rated among all views whose weight is greater than
246 * 0.
247 *
248 * Note non zero weights are only supported for Views that implement {@link ArcLayout
249 * .Widget}.
250 */
251 public void setWeight(float weight) {
252 mWeight = weight;
253 }
Xiangyin Made5091d2020-10-26 20:08:08 +0000254 }
255
256 /** Annotation for anchor types. */
257 /** @hide */
258 @Retention(RetentionPolicy.SOURCE)
259 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
260 @IntDef({ANCHOR_START, ANCHOR_CENTER, ANCHOR_END})
261 public @interface AnchorType {
262 }
263
264 /**
265 * Anchor at the start of the set of elements drawn within this container. This causes the first
266 * child to be drawn from {@code anchorAngle} degrees, to the right.
267 *
268 * <p>As an example, if this container contains two arcs, one having 10 degrees of sweep and the
269 * other having 20 degrees of sweep, the first will be drawn between 0-10 degrees, and the
270 * second between 10-30 degrees.
271 */
272 public static final int ANCHOR_START = 0;
273
274 /**
275 * Anchor at the center of the set of elements drawn within this container.
276 *
277 * <p>As an example, if this container contains two arcs, one having 10 degrees of sweep and the
278 * other having 20 degrees of sweep, the first will be drawn between -15 and -5 degrees, and the
279 * second between -5 and 15 degrees.
280 */
281 public static final int ANCHOR_CENTER = 1;
282
283 /**
284 * Anchor at the end of the set of elements drawn within this container. This causes the last
285 * element to end at {@code anchorAngle} degrees, with the other elements swept to the left.
286 *
287 * <p>As an example, if this container contains two arcs, one having 10 degrees of sweep and the
288 * other having 20 degrees of sweep, the first will be drawn between -30 and -20 degrees, and
289 * the second between -20 and 0 degrees.
290 */
291 public static final int ANCHOR_END = 2;
292
293 private static final float DEFAULT_START_ANGLE_DEGREES = 0f;
294 private static final boolean DEFAULT_LAYOUT_DIRECTION_IS_CLOCKWISE = true; // clockwise
295 @AnchorType
296 private static final int DEFAULT_ANCHOR_TYPE = ANCHOR_START;
297
298 private int mThicknessPx = 0;
299
300 @AnchorType
301 private int mAnchorType;
302 private float mAnchorAngleDegrees;
Alex Clarke975ffbd2022-07-05 10:14:48 +0100303 /**
304 * This is the target angle that will be used by the layout when expanding child views with
305 * weights.
306 */
307 private float mMaxAngleDegrees = 360.0f;
308
Xiangyin Made5091d2020-10-26 20:08:08 +0000309 private boolean mClockwise;
310
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000311 @SuppressWarnings("SyntheticAccessor")
312 private final ChildArcAngles mChildArcAngles = new ChildArcAngles();
Xiangyin Made5091d2020-10-26 20:08:08 +0000313
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100314 public ArcLayout(@NonNull Context context) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000315 this(context, null);
316 }
317
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100318 public ArcLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000319 this(context, attrs, 0);
320 }
321
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100322 public ArcLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000323 this(context, attrs, defStyleAttr, 0);
324 }
325
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100326 public ArcLayout(
Xiangyin Made5091d2020-10-26 20:08:08 +0000327 @NonNull Context context,
328 @Nullable AttributeSet attrs,
329 int defStyleAttr,
330 int defStyleRes) {
331 super(context, attrs, defStyleAttr, defStyleRes);
332
333 TypedArray a =
334 context.obtainStyledAttributes(
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100335 attrs, R.styleable.ArcLayout, defStyleAttr, defStyleRes
Xiangyin Made5091d2020-10-26 20:08:08 +0000336 );
337
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100338 mAnchorType = a.getInt(R.styleable.ArcLayout_anchorPosition, DEFAULT_ANCHOR_TYPE);
Xiangyin Made5091d2020-10-26 20:08:08 +0000339 mAnchorAngleDegrees =
340 a.getFloat(
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100341 R.styleable.ArcLayout_anchorAngleDegrees, DEFAULT_START_ANGLE_DEGREES
Xiangyin Made5091d2020-10-26 20:08:08 +0000342 );
343 mClockwise = a.getBoolean(
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100344 R.styleable.ArcLayout_clockwise, DEFAULT_LAYOUT_DIRECTION_IS_CLOCKWISE
Xiangyin Made5091d2020-10-26 20:08:08 +0000345 );
346
347 a.recycle();
348 }
349
350 @Override
Xiangyin Ma7db45322020-11-17 14:15:44 +0000351 public void requestLayout() {
352 super.requestLayout();
353
354 for (int i = 0; i < getChildCount(); i++) {
355 getChildAt(i).forceLayout();
356 }
357 }
358
359 @Override
Xiangyin Made5091d2020-10-26 20:08:08 +0000360 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
361 // Need to derive the thickness of the curve from the children. We're a curve, so the
362 // children can only be sized up to (width or height)/2 units. This currently only
363 // supports fitting to a circle.
364 //
365 // No matter what, fit to the given size, be it a maximum or a fixed size. It doesn't make
366 // sense for this container to wrap its children.
367 int actualWidthPx = MeasureSpec.getSize(widthMeasureSpec);
368 int actualHeightPx = MeasureSpec.getSize(heightMeasureSpec);
369
370 if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
371 && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
372 // We can't actually resolve this.
373 // Let's fit to the screen dimensions, for need of anything better...
374 DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
375 actualWidthPx = displayMetrics.widthPixels;
376 actualHeightPx = displayMetrics.heightPixels;
377 }
378
379 // Fit to a square.
380 if (actualWidthPx < actualHeightPx) {
381 actualHeightPx = actualWidthPx;
382 } else if (actualHeightPx < actualWidthPx) {
383 actualWidthPx = actualHeightPx;
384 }
385
386 int maxChildDimension = actualHeightPx / 2;
387
388 // Measure all children in the new measurespec, and cache the largest.
389 int childMeasureSpec = MeasureSpec.makeMeasureSpec(maxChildDimension, MeasureSpec.AT_MOST);
390
391 // We need to do two measure passes. First, we need to measure all "normal" children, and
392 // get the thickness of all "CurvedContainer" children. Once we have that, we know the
393 // maximum thickness, and we can lay out the "CurvedContainer" children, taking into
394 // account their vertical alignment.
395 int maxChildHeightPx = 0;
396 int childState = 0;
397 for (int i = 0; i < getChildCount(); i++) {
398 View child = getChildAt(i);
399
400 if (child.getVisibility() == GONE) {
401 continue;
402 }
403
404 // ArcLayoutWidget is a special case. Because of how it draws, fit it to the size
405 // of the whole widget.
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000406 int childMeasuredHeight;
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100407 if (child instanceof Widget) {
408 childMeasuredHeight = ((Widget) child).getThickness();
Xiangyin Made5091d2020-10-26 20:08:08 +0000409 } else {
410 measureChild(
411 child,
412 getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().width),
413 getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().height)
414 );
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000415 childMeasuredHeight = child.getMeasuredHeight();
Xiangyin Made5091d2020-10-26 20:08:08 +0000416 childState = combineMeasuredStates(childState, child.getMeasuredState());
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000417
Xiangyin Made5091d2020-10-26 20:08:08 +0000418 }
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000419 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
420 maxChildHeightPx = max(maxChildHeightPx, childMeasuredHeight
Alex Clarke975ffbd2022-07-05 10:14:48 +0100421 + childLayoutParams.topMargin + childLayoutParams.bottomMargin);
Xiangyin Made5091d2020-10-26 20:08:08 +0000422 }
423
424 mThicknessPx = maxChildHeightPx;
425
426 // And now do the pass for the ArcLayoutWidgets
427 for (int i = 0; i < getChildCount(); i++) {
428 View child = getChildAt(i);
429
430 if (child.getVisibility() == GONE) {
431 continue;
432 }
433
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100434 if (child instanceof Widget) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000435 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
436
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000437 float insetPx = getChildTopInset(child);
Xiangyin Made5091d2020-10-26 20:08:08 +0000438
439 int innerChildMeasureSpec =
440 MeasureSpec.makeMeasureSpec(
441 maxChildDimension * 2 - round(insetPx * 2), MeasureSpec.EXACTLY);
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000442
Xiangyin Made5091d2020-10-26 20:08:08 +0000443 measureChild(
444 child,
445 getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.width),
446 getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.height)
447 );
448
449 childState = combineMeasuredStates(childState, child.getMeasuredState());
450 }
451 }
452
453 setMeasuredDimension(
454 resolveSizeAndState(actualWidthPx, widthMeasureSpec, childState),
455 resolveSizeAndState(actualHeightPx, heightMeasureSpec, childState));
456 }
457
458 @Override
459 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Sergio Sancho72ca10e2022-01-25 16:41:45 +0000460 final boolean isLayoutRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
461
462 // != is equivalent to xor, we want to invert clockwise when the layout is rtl
463 final float multiplier = mClockwise != isLayoutRtl ? 1f : -1f;
464
Alex Clarke975ffbd2022-07-05 10:14:48 +0100465 // Layout the children in the arc, computing the center angle where they should be drawn.
Sergio Sancho72ca10e2022-01-25 16:41:45 +0000466 float currentCumulativeAngle = calculateInitialRotation(multiplier);
Alex Clarke975ffbd2022-07-05 10:14:48 +0100467
468 // Compute the sum of any weights and the sum of the angles take up by fixed sized children.
469 // Unfortunately we can't move this to measure because calculateArcAngle relies upon
470 // getMeasuredWidth() which returns 0 in measure.
471 float totalAngle = 0f;
472 float weightSum = 0f;
473 for (int i = 0; i < getChildCount(); i++) {
474 View child = getChildAt(i);
475
476 if (child.getVisibility() == GONE) {
477 continue;
478 }
479
480 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
481 if (childLayoutParams.mWeight > 0) {
482 weightSum += childLayoutParams.mWeight;
483 calculateArcAngle(child, mChildArcAngles);
484 totalAngle +=
485 mChildArcAngles.leftMarginAsAngle + mChildArcAngles.rightMarginAsAngle;
486 } else {
487 calculateArcAngle(child, mChildArcAngles);
488 totalAngle += mChildArcAngles.getTotalAngle();
489 }
490 }
491
492 float weightMultiplier = 0f;
493 if (weightSum > 0f) {
494 weightMultiplier = (mMaxAngleDegrees - totalAngle) / weightSum;
495 }
496
497 // Now perform the layout.
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000498 for (int i = 0; i < getChildCount(); i++) {
499 View child = getChildAt(i);
500
501 if (child.getVisibility() == GONE) {
502 continue;
503 }
504
505 calculateArcAngle(child, mChildArcAngles);
Alex Clarke975ffbd2022-07-05 10:14:48 +0100506 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
507 if (childLayoutParams.mWeight > 0) {
508 mChildArcAngles.actualChildAngle = childLayoutParams.mWeight * weightMultiplier;
509 if (child instanceof Widget) {
510 // NB we need to be careful since the child itself may set this value dueing
511 // measure.
512 ((Widget) child).setSweepAngleDegrees(mChildArcAngles.actualChildAngle);
513 } else {
514 throw new IllegalStateException("ArcLayout.LayoutParams with non zero weights"
515 + " are only supported for views implementing ArcLayout.Widget");
516 }
517 }
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000518 float preRotation = mChildArcAngles.leftMarginAsAngle
519 + mChildArcAngles.actualChildAngle / 2f;
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000520 float middleAngle = multiplier * (currentCumulativeAngle + preRotation);
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000521 childLayoutParams.mMiddleAngle = middleAngle;
522
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100523 // Distance from the center of the ArcLayout to the center of the child widget
524 float centerToCenterDistance = (getMeasuredHeight() - child.getMeasuredHeight()) / 2
525 - getChildTopInset(child);
526 // Move the center of the widget in the circle centered on this ArcLayout, and with
527 // radius centerToCenterDistance
528 childLayoutParams.mCenterX =
529 (float) (getMeasuredWidth() / 2f
530 + centerToCenterDistance * Math.sin(middleAngle * Math.PI / 180));
531 childLayoutParams.mCenterY =
532 (float) (getMeasuredHeight() / 2f
533 - centerToCenterDistance * Math.cos(middleAngle * Math.PI / 180));
534
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000535 currentCumulativeAngle += mChildArcAngles.getTotalAngle();
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100536
537 // Curved container widgets have been measured so that the "arc" inside their widget
538 // will touch the outside of the box they have been measured in, taking into account
539 // the vertical alignment. Just grow them from the center.
540 if (child instanceof Widget) {
541 int leftPx =
542 round((getMeasuredWidth() / 2f) - (child.getMeasuredWidth() / 2f));
543 int topPx =
544 round((getMeasuredHeight() / 2f) - (child.getMeasuredHeight() / 2f));
545
546 child.layout(
547 leftPx,
548 topPx,
549 leftPx + child.getMeasuredWidth(),
550 topPx + child.getMeasuredHeight()
551 );
552 } else {
553 // Normal widget's centers need to be placed on their final position,
554 // the only thing left for drawing is to maybe rotate them.
555 int leftPx = round(childLayoutParams.mCenterX - child.getMeasuredWidth() / 2f);
556 int topPx = round(childLayoutParams.mCenterY - child.getMeasuredHeight() / 2f);
557
558 child.layout(leftPx, topPx, leftPx + child.getMeasuredWidth(),
559 topPx + child.getMeasuredHeight());
560 }
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000561 }
Xiangyin Made5091d2020-10-26 20:08:08 +0000562 }
563
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000564 // When a view (that can handle it) receives a TOUCH_DOWN event, it will get all subsequent
565 // events until the touch is released, even if the pointer goes outside of it's bounds.
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000566 private View mTouchedView = null;
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000567
568 @Override
569 public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000570 if (mTouchedView == null && event.getActionMasked() == MotionEvent.ACTION_DOWN) {
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000571 for (int i = 0; i < getChildCount(); i++) {
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000572 View child = getChildAt(i);
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000573 // Ensure that the view is visible
574 if (child.getVisibility() != VISIBLE) {
575 continue;
576 }
577
578 // Map the event to the child's coordinate system
579 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
580 float angle = childLayoutParams.mMiddleAngle;
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000581
582 float[] point = new float[]{event.getX(), event.getY()};
583 mapPoint(child, angle, point);
584
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000585 // Check if the click is actually in the child area
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000586 float x = point[0];
587 float y = point[1];
588
589 if (insideChildClickArea(child, x, y)) {
590 mTouchedView = child;
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000591 break;
592 }
593 }
594 }
595 // We can't do normal dispatching because it will capture touch in the original position
596 // of children.
597 return true;
598 }
599
600 private static boolean insideChildClickArea(View child, float x, float y) {
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100601 if (child instanceof Widget) {
602 return ((Widget) child).isPointInsideClickArea(x, y);
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000603 }
604 return x >= 0 && x < child.getMeasuredWidth() && y >= 0 && y < child.getMeasuredHeight();
605 }
606
607 // Map a point to local child coordinates.
608 private void mapPoint(View child, float angle, float[] point) {
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000609 Matrix m = new Matrix();
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100610
611 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
612 if (child instanceof Widget) {
613 m.postRotate(-angle, getMeasuredWidth() / 2, getMeasuredHeight() / 2);
614 m.postTranslate(-child.getX(), -child.getY());
615 } else {
616 m.postTranslate(-childLayoutParams.mCenterX, -childLayoutParams.mCenterY);
617 if (childLayoutParams.isRotated()) {
618 m.postRotate(-angle);
Sergio Sanchoa4b24832020-11-24 11:20:35 +0000619 }
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100620 m.postTranslate(child.getWidth() / 2, child.getHeight() / 2);
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000621 }
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000622 m.mapPoints(point);
623 }
624
625 @Override
626 @SuppressLint("ClickableViewAccessibility")
627 public boolean onTouchEvent(@NonNull MotionEvent event) {
628 if (mTouchedView != null) {
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000629 // Map the event's coordinates to the child's coordinate space
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000630 float[] point = new float[]{event.getX(), event.getY()};
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000631 LayoutParams touchedViewLayoutParams = (LayoutParams) mTouchedView.getLayoutParams();
632 mapPoint(mTouchedView, touchedViewLayoutParams.mMiddleAngle, point);
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000633
634 float dx = point[0] - event.getX();
635 float dy = point[1] - event.getY();
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000636 event.offsetLocation(dx, dy);
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000637
638 mTouchedView.dispatchTouchEvent(event);
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000639
640 if (event.getActionMasked() == MotionEvent.ACTION_UP
641 || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
642 // We have finished handling these series of events.
643 mTouchedView = null;
644 }
645 return true;
646 }
647 return false;
648 }
649
Xiangyin Made5091d2020-10-26 20:08:08 +0000650 @Override
651 protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
652 // Rotate the canvas to make the children render in the right place.
653 canvas.save();
654
Sergio Sanchoe3b0e422021-01-08 15:07:05 +0000655 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
656 float middleAngle = childLayoutParams.mMiddleAngle;
Sergio Sancho0863d9c2020-11-17 11:28:45 +0000657
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100658 if (child instanceof Widget) {
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100659 // Rotate the child widget. This rotation places child widget in its correct place in
660 // the circle. Rotation is done around the center of the circle that components make.
661 canvas.rotate(
662 middleAngle,
663 getMeasuredWidth() / 2f,
664 getMeasuredHeight() / 2f);
665
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100666 ((Widget) child).checkInvalidAttributeAsChild();
Xiangyin Made5091d2020-10-26 20:08:08 +0000667 } else {
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100668 // Normal components already have their center in the right position during layout,
669 // the only thing remaining is any needed rotation.
670 // This rotation is done in place around the center of the
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000671 // child to adjust it based on rotation and clockwise attributes.
Sergio Sanchoe5400ec2021-05-11 14:16:16 +0100672 float angleToRotate = childLayoutParams.isRotated()
673 ? middleAngle + (mClockwise ? 0f : 180f)
674 : 0f;
675
676 canvas.rotate(angleToRotate, childLayoutParams.mCenterX, childLayoutParams.mCenterY);
Xiangyin Made5091d2020-10-26 20:08:08 +0000677 }
Xiangyin Made5091d2020-10-26 20:08:08 +0000678 boolean wasInvalidateIssued = super.drawChild(canvas, child, drawingTime);
679
680 canvas.restore();
681
682 return wasInvalidateIssued;
683 }
684
Sergio Sancho72ca10e2022-01-25 16:41:45 +0000685 private float calculateInitialRotation(float multiplier) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000686 if (mAnchorType == ANCHOR_START) {
687 return multiplier * mAnchorAngleDegrees;
688 }
689
690 float totalArcAngle = 0;
691
Alex Clarke975ffbd2022-07-05 10:14:48 +0100692 boolean hasWeights = false;
Xiangyin Made5091d2020-10-26 20:08:08 +0000693 for (int i = 0; i < getChildCount(); i++) {
Alex Clarke975ffbd2022-07-05 10:14:48 +0100694 View child = getChildAt(i);
695 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
696 if (childLayoutParams.getWeight() > 0f) {
697 hasWeights = true;
698 }
699 calculateArcAngle(child, mChildArcAngles);
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000700 totalArcAngle += mChildArcAngles.getTotalAngle();
Xiangyin Made5091d2020-10-26 20:08:08 +0000701 }
702
Alex Clarke975ffbd2022-07-05 10:14:48 +0100703 if (hasWeights && totalArcAngle < mMaxAngleDegrees) {
704 totalArcAngle = mMaxAngleDegrees;
705 }
706
Xiangyin Made5091d2020-10-26 20:08:08 +0000707 if (mAnchorType == ANCHOR_CENTER) {
708 return multiplier * mAnchorAngleDegrees - (totalArcAngle / 2f);
709 } else if (mAnchorType == ANCHOR_END) {
710 return multiplier * mAnchorAngleDegrees - totalArcAngle;
711 }
712
713 return 0;
714 }
715
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000716 private static float widthToAngleDegrees(float widthPx, float radiusPx) {
717 return (float) Math.toDegrees(2 * asin(widthPx / radiusPx / 2f));
718 }
719
720 private void calculateArcAngle(@NonNull View view, @NonNull ChildArcAngles childAngles) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000721 if (view.getVisibility() == GONE) {
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000722 childAngles.leftMarginAsAngle = 0;
723 childAngles.rightMarginAsAngle = 0;
724 childAngles.actualChildAngle = 0;
725 return;
Xiangyin Made5091d2020-10-26 20:08:08 +0000726 }
727
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000728 float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
729
730 LayoutParams childLayoutParams = (LayoutParams) view.getLayoutParams();
731
732 childAngles.leftMarginAsAngle =
733 widthToAngleDegrees(childLayoutParams.leftMargin, radiusPx);
734 childAngles.rightMarginAsAngle =
735 widthToAngleDegrees(childLayoutParams.rightMargin, radiusPx);
736
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100737 if (view instanceof Widget) {
738 childAngles.actualChildAngle = ((Widget) view).getSweepAngleDegrees();
Xiangyin Made5091d2020-10-26 20:08:08 +0000739 } else {
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000740 childAngles.actualChildAngle =
741 widthToAngleDegrees(view.getMeasuredWidth(), radiusPx);
Xiangyin Made5091d2020-10-26 20:08:08 +0000742 }
743 }
744
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000745 private float getChildTopInset(@NonNull View child) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000746 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
747
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100748 int childHeight = child instanceof Widget
749 ? ((Widget) child).getThickness()
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000750 : child.getMeasuredHeight();
751
752 int thicknessDiffPx =
753 mThicknessPx - childLayoutParams.topMargin - childLayoutParams.bottomMargin
754 - childHeight;
755
756 int margin = mClockwise ? childLayoutParams.topMargin : childLayoutParams.bottomMargin;
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000757 float topInset = margin + getChildTopOffset(child);
Xiangyin Made5091d2020-10-26 20:08:08 +0000758
759 switch (childLayoutParams.getVerticalAlignment()) {
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100760 case LayoutParams.VERTICAL_ALIGN_OUTER:
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000761 return topInset;
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100762 case LayoutParams.VERTICAL_ALIGN_CENTER:
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000763 return topInset + thicknessDiffPx / 2f;
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100764 case LayoutParams.VERTICAL_ALIGN_INNER:
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000765 return topInset + thicknessDiffPx;
Xiangyin Made5091d2020-10-26 20:08:08 +0000766 default:
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000767 // Normally unreachable...
Xiangyin Made5091d2020-10-26 20:08:08 +0000768 return 0;
769 }
770 }
771
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000772 /**
773 * For vertical rectangular screens, additional offset needs to be taken into the account for
774 * y position of normal widget in order to be in the correct place in the circle.
775 */
776 private float getChildTopOffset(View child) {
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100777 if (child instanceof Widget || getMeasuredWidth() >= getMeasuredHeight()) {
Neda Topoljanaca83d8af2021-02-23 20:52:16 +0000778 return 0;
779 }
780 return round((getMeasuredHeight() - getMeasuredWidth()) / 2f);
781 }
782
Xiangyin Made5091d2020-10-26 20:08:08 +0000783 @Override
784 protected boolean checkLayoutParams(@NonNull ViewGroup.LayoutParams p) {
785 return p instanceof LayoutParams;
786 }
787
788 @Override
789 @NonNull
790 protected ViewGroup.LayoutParams generateLayoutParams(@NonNull ViewGroup.LayoutParams p) {
791 return new LayoutParams(p);
792 }
793
794 @Override
795 @NonNull
796 public ViewGroup.LayoutParams generateLayoutParams(@NonNull AttributeSet attrs) {
797 return new LayoutParams(getContext(), attrs);
798 }
799
800 @Override
801 @NonNull
802 protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
803 return new LayoutParams(
804 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
805 }
806
807 /** Returns the anchor type used for this container. */
808 @AnchorType
809 public int getAnchorType() {
810 return mAnchorType;
811 }
812
813 /** Sets the anchor type used for this container. */
814 public void setAnchorType(@AnchorType int anchorType) {
815 if (anchorType < ANCHOR_START || anchorType > ANCHOR_END) {
816 throw new IllegalArgumentException("Unknown anchor type");
817 }
818
819 mAnchorType = anchorType;
820 invalidate();
821 }
822
823 /** Returns the anchor angle used for this container, in degrees. */
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100824 @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
Xiangyin Made5091d2020-10-26 20:08:08 +0000825 public float getAnchorAngleDegrees() {
826 return mAnchorAngleDegrees;
827 }
828
829 /** Sets the anchor angle used for this container, in degrees. */
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100830 public void setAnchorAngleDegrees(
831 @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) float anchorAngleDegrees) {
Xiangyin Made5091d2020-10-26 20:08:08 +0000832 mAnchorAngleDegrees = anchorAngleDegrees;
833 invalidate();
834 }
835
Alex Clarke975ffbd2022-07-05 10:14:48 +0100836 /**
837 * Returns the target angle that will be used by the layout when expanding child views with
838 * weights (see {@link LayoutParams#setWeight}).
839 */
840 @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
841 public float getMaxAngleDegrees() {
842 return mMaxAngleDegrees;
843 }
844
845 /**
846 * Sets the target angle that will be used by the layout when expanding child views with
847 * weights (see {@link LayoutParams#setWeight}). If not set the default is 360 degrees. This
848 * target may not be achievable if other non-expandable views bring us past this value.
849 */
850 public void setMaxAngleDegrees(
851 @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
852 float maxAngleDegrees) {
853 mMaxAngleDegrees = maxAngleDegrees;
854 invalidate();
855 requestLayout();
856 }
857
Xiangyin Made5091d2020-10-26 20:08:08 +0000858 /** returns the layout direction */
Sergio Sancho25fc17f2021-04-27 18:03:58 +0100859 public boolean isClockwise() {
Xiangyin Made5091d2020-10-26 20:08:08 +0000860 return mClockwise;
861 }
862
863 /** Sets the layout direction */
864 public void setClockwise(boolean clockwise) {
865 mClockwise = clockwise;
866 invalidate();
867 }
Sergio Sanchodbac6fc2020-11-25 16:43:10 +0000868
869 private static class ChildArcAngles {
870 public float leftMarginAsAngle;
871 public float rightMarginAsAngle;
872 public float actualChildAngle;
873
874 public float getTotalAngle() {
875 return leftMarginAsAngle + rightMarginAsAngle + actualChildAngle;
876 }
877 }
Xiangyin Made5091d2020-10-26 20:08:08 +0000878}