Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package androidx.wear.widget; |
| 18 | |
| 19 | import static java.lang.Math.asin; |
| 20 | import static java.lang.Math.max; |
| 21 | import static java.lang.Math.round; |
| 22 | |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 23 | import android.annotation.SuppressLint; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 24 | import android.content.Context; |
| 25 | import android.content.res.TypedArray; |
| 26 | import android.graphics.Canvas; |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 27 | import android.graphics.Matrix; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 28 | import android.util.AttributeSet; |
| 29 | import android.util.DisplayMetrics; |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 30 | import android.view.MotionEvent; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 31 | import android.view.View; |
| 32 | import android.view.ViewGroup; |
| 33 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 34 | import androidx.annotation.FloatRange; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 35 | import androidx.annotation.IntDef; |
| 36 | import androidx.annotation.NonNull; |
| 37 | import androidx.annotation.Nullable; |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 38 | import androidx.annotation.Px; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 39 | import androidx.annotation.RestrictTo; |
| 40 | import androidx.annotation.UiThread; |
| 41 | import androidx.wear.R; |
| 42 | |
| 43 | import java.lang.annotation.Retention; |
| 44 | import 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 69 | * <p>Widgets which implement {@link ArcLayout.Widget} are expected to draw themselves within an arc |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 70 | * 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 73 | * ArcLayout will apply a rotation transform to the canvas before drawing this child; the |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 74 | * inner child need not perform any rotations itself. |
| 75 | * |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 76 | * <p>An example of a widget which implements this interface is {@link CurvedTextView}, which |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 77 | * will lay itself out along the arc. |
| 78 | */ |
| 79 | @UiThread |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 80 | public class ArcLayout extends ViewGroup { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 81 | |
| 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 87 | public interface Widget { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 88 | |
| 89 | /** Returns the sweep angle that this widget is drawn with. */ |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 90 | @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 91 | float getSweepAngleDegrees(); |
| 92 | |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 93 | /** |
| 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 102 | /** Returns the thickness of this widget inside the arc. */ |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 103 | @Px |
| 104 | int getThickness(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 105 | |
| 106 | /** |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 107 | * 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 112 | */ |
Sergio Sancho | 2798931 | 2020-11-24 17:14:07 +0000 | [diff] [blame] | 113 | void checkInvalidAttributeAsChild(); |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 114 | |
| 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 Sancho | 8059663 | 2021-04-22 13:22:02 +0100 | [diff] [blame] | 120 | boolean isPointInsideClickArea(float x, float y); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 121 | } |
| 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 Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 131 | public static class LayoutParams extends ViewGroup.MarginLayoutParams { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 132 | |
| 133 | /** Vertical alignment of elements within the arc. */ |
| 134 | /** @hide */ |
| 135 | @Retention(RetentionPolicy.SOURCE) |
| 136 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 137 | @IntDef({VERTICAL_ALIGN_OUTER, VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_INNER}) |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 138 | public @interface VerticalAlignment { |
| 139 | } |
| 140 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 141 | /** Align to the outer edge of the parent ArcLayout. */ |
| 142 | public static final int VERTICAL_ALIGN_OUTER = 0; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 143 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 144 | /** Align to the center of the parent ArcLayout. */ |
| 145 | public static final int VERTICAL_ALIGN_CENTER = 1; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 146 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 147 | /** Align to the inner edge of the parent ArcLayout. */ |
| 148 | public static final int VERTICAL_ALIGN_INNER = 2; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 149 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 150 | private boolean mRotated = true; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 151 | @VerticalAlignment |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 152 | private int mVerticalAlignment = VERTICAL_ALIGN_CENTER; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 153 | |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 154 | // Internally used during layout/draw |
| 155 | // Stores the angle of the child, used to handle touch events. |
| 156 | float mMiddleAngle; |
| 157 | |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 158 | // 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 Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 163 | // The layout weight for this view, a value of zero means no expansion. |
| 164 | float mWeight; |
| 165 | |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 166 | /** |
| 167 | * Creates a new set of layout parameters. The values are extracted from the supplied |
| 168 | * attributes set and context. |
| 169 | * |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 170 | * @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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 173 | */ |
| 174 | public LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) { |
| 175 | super(context, attrs); |
| 176 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 177 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcLayout_Layout); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 178 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 179 | mRotated = a.getBoolean(R.styleable.ArcLayout_Layout_layout_rotate, true); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 180 | mVerticalAlignment = |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 181 | a.getInt(R.styleable.ArcLayout_Layout_layout_valign, VERTICAL_ALIGN_CENTER); |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 182 | mWeight = a.getFloat(R.styleable.ArcLayout_Layout_layout_weight, 0f); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 183 | |
| 184 | a.recycle(); |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Creates a new set of layout parameters with specified width and height |
| 189 | * |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 190 | * @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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 192 | */ |
| 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 203 | * Gets whether the widget shall be rotated by the ArcLayout container corresponding |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 204 | * to its layout position angle |
| 205 | */ |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 206 | public boolean isRotated() { |
| 207 | return mRotated; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 208 | } |
| 209 | |
| 210 | /** |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 211 | * Sets whether the widget shall be rotated by the ArcLayout container corresponding |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 212 | * to its layout position angle |
| 213 | */ |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 214 | public void setRotated(boolean rotated) { |
| 215 | mRotated = rotated; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 216 | } |
| 217 | |
| 218 | /** |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 219 | * Gets how the widget is positioned vertically in the ArcLayout. |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 220 | */ |
| 221 | @VerticalAlignment |
| 222 | public int getVerticalAlignment() { |
| 223 | return mVerticalAlignment; |
| 224 | } |
| 225 | |
| 226 | /** |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 227 | * Sets how the widget is positioned vertically in the ArcLayout. |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 228 | * |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 229 | * @param verticalAlignment align the widget to outer, inner edges or center. |
| 230 | */ |
| 231 | public void setVerticalAlignment(@VerticalAlignment int verticalAlignment) { |
| 232 | mVerticalAlignment = verticalAlignment; |
| 233 | } |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 234 | |
| 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 254 | } |
| 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 Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 303 | /** |
| 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 309 | private boolean mClockwise; |
| 310 | |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 311 | @SuppressWarnings("SyntheticAccessor") |
| 312 | private final ChildArcAngles mChildArcAngles = new ChildArcAngles(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 313 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 314 | public ArcLayout(@NonNull Context context) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 315 | this(context, null); |
| 316 | } |
| 317 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 318 | public ArcLayout(@NonNull Context context, @Nullable AttributeSet attrs) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 319 | this(context, attrs, 0); |
| 320 | } |
| 321 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 322 | public ArcLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 323 | this(context, attrs, defStyleAttr, 0); |
| 324 | } |
| 325 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 326 | public ArcLayout( |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 327 | @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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 335 | attrs, R.styleable.ArcLayout, defStyleAttr, defStyleRes |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 336 | ); |
| 337 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 338 | mAnchorType = a.getInt(R.styleable.ArcLayout_anchorPosition, DEFAULT_ANCHOR_TYPE); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 339 | mAnchorAngleDegrees = |
| 340 | a.getFloat( |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 341 | R.styleable.ArcLayout_anchorAngleDegrees, DEFAULT_START_ANGLE_DEGREES |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 342 | ); |
| 343 | mClockwise = a.getBoolean( |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 344 | R.styleable.ArcLayout_clockwise, DEFAULT_LAYOUT_DIRECTION_IS_CLOCKWISE |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 345 | ); |
| 346 | |
| 347 | a.recycle(); |
| 348 | } |
| 349 | |
| 350 | @Override |
Xiangyin Ma | 7db4532 | 2020-11-17 14:15:44 +0000 | [diff] [blame] | 351 | 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 360 | 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 Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 406 | int childMeasuredHeight; |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 407 | if (child instanceof Widget) { |
| 408 | childMeasuredHeight = ((Widget) child).getThickness(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 409 | } else { |
| 410 | measureChild( |
| 411 | child, |
| 412 | getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().width), |
| 413 | getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().height) |
| 414 | ); |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 415 | childMeasuredHeight = child.getMeasuredHeight(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 416 | childState = combineMeasuredStates(childState, child.getMeasuredState()); |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 417 | |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 418 | } |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 419 | LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); |
| 420 | maxChildHeightPx = max(maxChildHeightPx, childMeasuredHeight |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 421 | + childLayoutParams.topMargin + childLayoutParams.bottomMargin); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 422 | } |
| 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 434 | if (child instanceof Widget) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 435 | LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); |
| 436 | |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 437 | float insetPx = getChildTopInset(child); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 438 | |
| 439 | int innerChildMeasureSpec = |
| 440 | MeasureSpec.makeMeasureSpec( |
| 441 | maxChildDimension * 2 - round(insetPx * 2), MeasureSpec.EXACTLY); |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 442 | |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 443 | 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 Sancho | 72ca10e | 2022-01-25 16:41:45 +0000 | [diff] [blame] | 460 | 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 Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 465 | // Layout the children in the arc, computing the center angle where they should be drawn. |
Sergio Sancho | 72ca10e | 2022-01-25 16:41:45 +0000 | [diff] [blame] | 466 | float currentCumulativeAngle = calculateInitialRotation(multiplier); |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 467 | |
| 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 Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 498 | 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 Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 506 | 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 Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 518 | float preRotation = mChildArcAngles.leftMarginAsAngle |
| 519 | + mChildArcAngles.actualChildAngle / 2f; |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 520 | float middleAngle = multiplier * (currentCumulativeAngle + preRotation); |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 521 | childLayoutParams.mMiddleAngle = middleAngle; |
| 522 | |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 523 | // 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 Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 535 | currentCumulativeAngle += mChildArcAngles.getTotalAngle(); |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 536 | |
| 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 Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 561 | } |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 562 | } |
| 563 | |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 564 | // 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 Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 566 | private View mTouchedView = null; |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 567 | |
| 568 | @Override |
| 569 | public boolean onInterceptTouchEvent(@NonNull MotionEvent event) { |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 570 | if (mTouchedView == null && event.getActionMasked() == MotionEvent.ACTION_DOWN) { |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 571 | for (int i = 0; i < getChildCount(); i++) { |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 572 | View child = getChildAt(i); |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 573 | // 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 Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 581 | |
| 582 | float[] point = new float[]{event.getX(), event.getY()}; |
| 583 | mapPoint(child, angle, point); |
| 584 | |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 585 | // Check if the click is actually in the child area |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 586 | float x = point[0]; |
| 587 | float y = point[1]; |
| 588 | |
| 589 | if (insideChildClickArea(child, x, y)) { |
| 590 | mTouchedView = child; |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 591 | 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 601 | if (child instanceof Widget) { |
| 602 | return ((Widget) child).isPointInsideClickArea(x, y); |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 603 | } |
| 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 Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 609 | Matrix m = new Matrix(); |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 610 | |
| 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 Sancho | a4b2483 | 2020-11-24 11:20:35 +0000 | [diff] [blame] | 619 | } |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 620 | m.postTranslate(child.getWidth() / 2, child.getHeight() / 2); |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 621 | } |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 622 | m.mapPoints(point); |
| 623 | } |
| 624 | |
| 625 | @Override |
| 626 | @SuppressLint("ClickableViewAccessibility") |
| 627 | public boolean onTouchEvent(@NonNull MotionEvent event) { |
| 628 | if (mTouchedView != null) { |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 629 | // Map the event's coordinates to the child's coordinate space |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 630 | float[] point = new float[]{event.getX(), event.getY()}; |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 631 | LayoutParams touchedViewLayoutParams = (LayoutParams) mTouchedView.getLayoutParams(); |
| 632 | mapPoint(mTouchedView, touchedViewLayoutParams.mMiddleAngle, point); |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 633 | |
| 634 | float dx = point[0] - event.getX(); |
| 635 | float dy = point[1] - event.getY(); |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 636 | event.offsetLocation(dx, dy); |
Sergio Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 637 | |
| 638 | mTouchedView.dispatchTouchEvent(event); |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 639 | |
| 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 650 | @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 Sancho | e3b0e42 | 2021-01-08 15:07:05 +0000 | [diff] [blame] | 655 | LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); |
| 656 | float middleAngle = childLayoutParams.mMiddleAngle; |
Sergio Sancho | 0863d9c | 2020-11-17 11:28:45 +0000 | [diff] [blame] | 657 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 658 | if (child instanceof Widget) { |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 659 | // 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 666 | ((Widget) child).checkInvalidAttributeAsChild(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 667 | } else { |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 668 | // 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 Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 671 | // child to adjust it based on rotation and clockwise attributes. |
Sergio Sancho | e5400ec | 2021-05-11 14:16:16 +0100 | [diff] [blame] | 672 | float angleToRotate = childLayoutParams.isRotated() |
| 673 | ? middleAngle + (mClockwise ? 0f : 180f) |
| 674 | : 0f; |
| 675 | |
| 676 | canvas.rotate(angleToRotate, childLayoutParams.mCenterX, childLayoutParams.mCenterY); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 677 | } |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 678 | boolean wasInvalidateIssued = super.drawChild(canvas, child, drawingTime); |
| 679 | |
| 680 | canvas.restore(); |
| 681 | |
| 682 | return wasInvalidateIssued; |
| 683 | } |
| 684 | |
Sergio Sancho | 72ca10e | 2022-01-25 16:41:45 +0000 | [diff] [blame] | 685 | private float calculateInitialRotation(float multiplier) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 686 | if (mAnchorType == ANCHOR_START) { |
| 687 | return multiplier * mAnchorAngleDegrees; |
| 688 | } |
| 689 | |
| 690 | float totalArcAngle = 0; |
| 691 | |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 692 | boolean hasWeights = false; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 693 | for (int i = 0; i < getChildCount(); i++) { |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 694 | View child = getChildAt(i); |
| 695 | LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); |
| 696 | if (childLayoutParams.getWeight() > 0f) { |
| 697 | hasWeights = true; |
| 698 | } |
| 699 | calculateArcAngle(child, mChildArcAngles); |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 700 | totalArcAngle += mChildArcAngles.getTotalAngle(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 701 | } |
| 702 | |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 703 | if (hasWeights && totalArcAngle < mMaxAngleDegrees) { |
| 704 | totalArcAngle = mMaxAngleDegrees; |
| 705 | } |
| 706 | |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 707 | 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 Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 716 | 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 721 | if (view.getVisibility() == GONE) { |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 722 | childAngles.leftMarginAsAngle = 0; |
| 723 | childAngles.rightMarginAsAngle = 0; |
| 724 | childAngles.actualChildAngle = 0; |
| 725 | return; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 726 | } |
| 727 | |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 728 | 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 737 | if (view instanceof Widget) { |
| 738 | childAngles.actualChildAngle = ((Widget) view).getSweepAngleDegrees(); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 739 | } else { |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 740 | childAngles.actualChildAngle = |
| 741 | widthToAngleDegrees(view.getMeasuredWidth(), radiusPx); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 742 | } |
| 743 | } |
| 744 | |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 745 | private float getChildTopInset(@NonNull View child) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 746 | LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); |
| 747 | |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 748 | int childHeight = child instanceof Widget |
| 749 | ? ((Widget) child).getThickness() |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 750 | : child.getMeasuredHeight(); |
| 751 | |
| 752 | int thicknessDiffPx = |
| 753 | mThicknessPx - childLayoutParams.topMargin - childLayoutParams.bottomMargin |
| 754 | - childHeight; |
| 755 | |
| 756 | int margin = mClockwise ? childLayoutParams.topMargin : childLayoutParams.bottomMargin; |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 757 | float topInset = margin + getChildTopOffset(child); |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 758 | |
| 759 | switch (childLayoutParams.getVerticalAlignment()) { |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 760 | case LayoutParams.VERTICAL_ALIGN_OUTER: |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 761 | return topInset; |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 762 | case LayoutParams.VERTICAL_ALIGN_CENTER: |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 763 | return topInset + thicknessDiffPx / 2f; |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 764 | case LayoutParams.VERTICAL_ALIGN_INNER: |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 765 | return topInset + thicknessDiffPx; |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 766 | default: |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 767 | // Normally unreachable... |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 768 | return 0; |
| 769 | } |
| 770 | } |
| 771 | |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 772 | /** |
| 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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 777 | if (child instanceof Widget || getMeasuredWidth() >= getMeasuredHeight()) { |
Neda Topoljanac | a83d8af | 2021-02-23 20:52:16 +0000 | [diff] [blame] | 778 | return 0; |
| 779 | } |
| 780 | return round((getMeasuredHeight() - getMeasuredWidth()) / 2f); |
| 781 | } |
| 782 | |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 783 | @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 Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 824 | @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 825 | public float getAnchorAngleDegrees() { |
| 826 | return mAnchorAngleDegrees; |
| 827 | } |
| 828 | |
| 829 | /** Sets the anchor angle used for this container, in degrees. */ |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 830 | public void setAnchorAngleDegrees( |
| 831 | @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) float anchorAngleDegrees) { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 832 | mAnchorAngleDegrees = anchorAngleDegrees; |
| 833 | invalidate(); |
| 834 | } |
| 835 | |
Alex Clarke | 975ffbd | 2022-07-05 10:14:48 +0100 | [diff] [blame^] | 836 | /** |
| 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 858 | /** returns the layout direction */ |
Sergio Sancho | 25fc17f | 2021-04-27 18:03:58 +0100 | [diff] [blame] | 859 | public boolean isClockwise() { |
Xiangyin Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 860 | return mClockwise; |
| 861 | } |
| 862 | |
| 863 | /** Sets the layout direction */ |
| 864 | public void setClockwise(boolean clockwise) { |
| 865 | mClockwise = clockwise; |
| 866 | invalidate(); |
| 867 | } |
Sergio Sancho | dbac6fc | 2020-11-25 16:43:10 +0000 | [diff] [blame] | 868 | |
| 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 Ma | de5091d | 2020-10-26 20:08:08 +0000 | [diff] [blame] | 878 | } |