WearCurvedTextView.java

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.widget;

import static java.lang.Math.cos;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;
import static java.lang.Math.sin;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Build;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.wear.R;

/**
 * A WearCurvedTextView is a component allowing developers to easily write curved text following
 * the curvature of the largest circle that can be inscribed in the view. WearArcLayout could be
 * used to concatenate multiple curved texts, also layout together with other widgets such as icons.
 */
public class WearCurvedTextView extends View implements WearArcLayout.ArcLayoutWidget {
    private static final float UNSET_ANCHOR_DEGREE = -1f;
    private static final int UNSET_ANCHOR_TYPE = -1;
    private static final float UNSET_SWEEP_DEGREE = -1f;
    private static final float MAX_SWEEP_DEGREE = 359.9f;
    private static final float DEFAULT_TEXT_SIZE = 24f;
    @ColorInt
    private static final int DEFAULT_TEXT_COLOR = Color.WHITE;
    private static final int DEFAULT_TEXT_STYLE = Typeface.NORMAL;
    private static final boolean DEFAULT_CLOCKWISE = true;
    private static final int FONT_WEIGHT_MAX = 1000;
    private static final float ITALIC_SKEW_X = -0.25f;
    // make 0 degree at 12 o'clock, since canvas assumes 0 degree is 3 o'clock
    private static final float ANCHOR_DEGREE_OFFSET = -90f;

    private final Path mPath = new Path();
    private final Path mBgPath = new Path();
    private final TextPaint mPaint = new TextPaint();
    private final Rect mBounds = new Rect();
    private final Rect mBgBounds = new Rect();
    private boolean mDirty = true;
    private String mTextToDraw = "";
    private float mPathRadius = 0f;
    private float mTextSweepDegrees = 0f;
    private float mBackgroundSweepDegrees = MAX_SWEEP_DEGREE;
    private boolean mHasParentArcLayout = false;
    private boolean mParentClockwise = true;
    private int mLastUsedTextAlignment = -1;
    private float mLocalRotateAngle = 0f;
    private float mParentRotateAngle = 0f;

    private int mAnchorType = UNSET_ANCHOR_TYPE;
    private float mAnchorAngleDegrees = UNSET_ANCHOR_DEGREE;
    private float mSweepDegrees = UNSET_SWEEP_DEGREE;
    private String mText = "";
    private float mTextSize = DEFAULT_TEXT_SIZE;
    @Nullable
    private Typeface mTypeface = null;
    private boolean mClockwise = DEFAULT_CLOCKWISE;
    @ColorInt
    private int mTextColor = DEFAULT_TEXT_COLOR;
    @Nullable
    private TextUtils.TruncateAt mEllipsize = null;
    private float mLetterSpacing = 0f;
    @Nullable
    private String mFontFeatureSettings = null;
    @Nullable
    private String mFontVariationSettings = null;

    public WearCurvedTextView(@NonNull Context context) {
        this(context, null);
    }

    public WearCurvedTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

    public WearCurvedTextView(
            @NonNull Context context,
            @Nullable AttributeSet attrs,
            int defStyle) {
        this(context, attrs, defStyle, 0);
    }

    public WearCurvedTextView(
            @NonNull Context context,
            @Nullable AttributeSet attrs,
            int defStyle,
            int defStyleRes) {
        super(context, attrs, defStyle, defStyleRes);

        mPaint.setAntiAlias(true);

        TextAppearanceAttributes attributes = new TextAppearanceAttributes();
        attributes.mTextColor = ColorStateList.valueOf(DEFAULT_TEXT_COLOR);

        final Resources.Theme theme = context.getTheme();
        TypedArray a = theme.obtainStyledAttributes(
                attrs, R.styleable.TextViewAppearance, defStyle, defStyleRes);

        TypedArray appearance = null;
        int ap = a.getResourceId(R.styleable.TextViewAppearance_android_textAppearance, -1);
        a.recycle();

        if (ap != -1) {
            appearance = theme.obtainStyledAttributes(ap, R.styleable.TextAppearance);
        }
        if (appearance != null) {
            readTextAppearance(appearance, attributes, true);
            appearance.recycle();
        }

        a = context.obtainStyledAttributes(
                attrs, R.styleable.WearCurvedTextView, defStyle, defStyleRes);
        // overrride the value in the appearance with explicitly specified attribute values
        readTextAppearance(a, attributes, false);

        // read the other supported TextView attributes
        if (a.hasValue(R.styleable.WearCurvedTextView_android_text)) {
            mText = a.getString(R.styleable.WearCurvedTextView_android_text);
        }

        int textEllipsize = a.getInt(R.styleable.WearCurvedTextView_android_ellipsize, 0);
        switch (textEllipsize) {
            case 1:
                mEllipsize = TextUtils.TruncateAt.START;
                break;
            case 2:
                mEllipsize = TextUtils.TruncateAt.MIDDLE;
                break;
            case 3:
                mEllipsize = TextUtils.TruncateAt.END;
                break;
            default:
                mEllipsize = null;
        }

        // read the custom WearCurvedTextView attributes
        mSweepDegrees = a.getFloat(R.styleable.WearCurvedTextView_sweepDegrees, UNSET_SWEEP_DEGREE);
        mAnchorType = a.getInt(R.styleable.WearCurvedTextView_anchorPosition, UNSET_ANCHOR_TYPE);
        mAnchorAngleDegrees = a.getFloat(
                R.styleable.WearCurvedTextView_anchorAngleDegrees, UNSET_ANCHOR_DEGREE
        );
        mAnchorAngleDegrees = mAnchorAngleDegrees % 360f;
        mClockwise = a.getBoolean(R.styleable.WearCurvedTextView_clockwise, DEFAULT_CLOCKWISE);

        a.recycle();

        applyTextAppearance(attributes);
    }

    @Override
    public float getSweepAngleDegrees() {
        return mBackgroundSweepDegrees;
    }

    @Override
    public int getThicknessPx() {
        return round(mPaint.getFontMetrics().descent - mPaint.getFontMetrics().ascent);
    }

    /**
     * @throws IllegalArgumentException if the anchorType and/or anchorAngleDegrees attributes
     *                                  were set for a widget in WearArcLayout
     */
    @Override
    public void checkInvalidAttributeAsChild(boolean parentClockwise) {
        this.mHasParentArcLayout = true;
        this.mParentClockwise = parentClockwise;

        if (mAnchorType != UNSET_ANCHOR_TYPE) {
            throw new IllegalArgumentException(
                    "WearCurvedTextView shall not set anchorType value when added into"
                            + "WearArcLayout"
            );
        }

        if (mAnchorAngleDegrees != UNSET_ANCHOR_DEGREE) {
            throw new IllegalArgumentException(
                    "WearCurvedTextView shall not set anchorAngleDegrees value when added into "
                            + "WearArcLayout"
            );
        }
    }

    @Override
    public boolean handleLayoutRotate(float angle) {
        mParentRotateAngle = angle;
        return true;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        doUpdate();
    }

    private void updatePaint() {
        mPaint.setTextSize(mTextSize);
        mPaint.getTextBounds(mText, 0, mText.length(), mBounds);

        // Note that ascent is negative.

        mPathRadius = min(getWidth(), getHeight()) / 2f
                + (mClockwise ? mPaint.getFontMetrics().ascent - getPaddingTop() :
                -mPaint.getFontMetrics().descent - getPaddingBottom());
        mTextSweepDegrees = min(
                getWidthSelf() / mPathRadius / (float) Math.PI * 180f,
                MAX_SWEEP_DEGREE);
        mBackgroundSweepDegrees =
                (mSweepDegrees == UNSET_SWEEP_DEGREE) ? mTextSweepDegrees
                        : min(mSweepDegrees, MAX_SWEEP_DEGREE);
    }

    private float getWidthSelf() {
        return (float) mBounds.width() + getPaddingLeft() + getPaddingRight();
    }

    private String ellipsize(int ellipsizedWidth) {
        StaticLayout.Builder layoutBuilder =
                StaticLayout.Builder.obtain(mText, 0, mText.length(), mPaint, ellipsizedWidth);
        layoutBuilder.setEllipsize(mEllipsize);
        layoutBuilder.setMaxLines(1);
        StaticLayout layout = layoutBuilder.build();

        // Cut text that it's too big even if no ellipsize mode is provided.
        if (mEllipsize == null) {
            return mText.substring(0, layout.getLineEnd(0));
        }

        int ellipsisCount = layout.getEllipsisCount(0);
        if (ellipsisCount == 0) {
            return mText;
        }

        int ellipsisStart = layout.getEllipsisStart(0);
        char[] textToDrawArray = mText.toCharArray();
        textToDrawArray[ellipsisStart] = '\u2026'; // ellipsis "..."
        for (int i = ellipsisStart + 1; i < ellipsisStart + ellipsisCount; i++) {
            if (i >= 0 && i < mText.length()) {
                textToDrawArray[i] = '\uFEFF'; // 0-width space
            }
        }
        return new String(textToDrawArray);
    }

    private void updatePathsIfNeeded(boolean withBackground) {
        // The dirty flag is not set when properties we inherit from View are modified
        if (!mDirty && ((int) getTextAlignment() == mLastUsedTextAlignment)) {
            return;
        }

        mDirty = false;
        mLastUsedTextAlignment = (int) getTextAlignment();
        mPaint.setTextSize(mTextSize);

        float maxSweepDegrees =
                mSweepDegrees == UNSET_SWEEP_DEGREE ? MAX_SWEEP_DEGREE : mSweepDegrees;
        if (mTextSweepDegrees <= maxSweepDegrees) {
            mTextToDraw = mText;
        } else {
            mTextToDraw = ellipsize(
                    (int) (maxSweepDegrees / 180f * Math.PI * mPathRadius) - getPaddingLeft()
                            - getPaddingRight()
            );
            mTextSweepDegrees = maxSweepDegrees;
        }

        float clockwiseFactor = mClockwise ? 1f : -1f;
        float parentClockwiseFactor =
                mHasParentArcLayout ? (mParentClockwise ? 1f : -1f) : clockwiseFactor;

        float alignmentFactor = 0.5f;
        switch (getTextAlignment()) {
            case TEXT_ALIGNMENT_TEXT_START:
            case TEXT_ALIGNMENT_VIEW_START:
                alignmentFactor = 0f;
                break;
            case TEXT_ALIGNMENT_TEXT_END:
            case TEXT_ALIGNMENT_VIEW_END:
                alignmentFactor = 1f;
                break;
            default:
                alignmentFactor = 0.5f; // TEXT_ALIGNMENT_CENTER
        }

        float anchorTypeFactor = 0f;
        switch (mAnchorType) {
            case WearArcLayout.ANCHOR_START:
                anchorTypeFactor = 0f;
                break;
            case WearArcLayout.ANCHOR_CENTER:
                anchorTypeFactor = 0.5f;
                break;
            case WearArcLayout.ANCHOR_END:
                anchorTypeFactor = 1f;
                break;
            default:
                anchorTypeFactor = parentClockwiseFactor == clockwiseFactor ? 0f : -1f;
        }

        float actualAnchorDegree =
                (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
                        + ANCHOR_DEGREE_OFFSET;

        // Always draw the curved text on top center, then rotate the canvas to the right position
        float backgroundStartAngle =
                -clockwiseFactor * 0.5f * mBackgroundSweepDegrees + ANCHOR_DEGREE_OFFSET;
        mLocalRotateAngle =
                actualAnchorDegree - backgroundStartAngle
                        - parentClockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;

        float textStartAngle =
                backgroundStartAngle + clockwiseFactor * (float) (
                        alignmentFactor * (mBackgroundSweepDegrees - mTextSweepDegrees)
                                + getPaddingLeft() / mPathRadius / Math.PI * 180);

        float centerX = getWidth() / 2f;
        float centerY = getHeight() / 2f;
        mPath.reset();
        mPath.addArc(
                centerX - mPathRadius,
                centerY - mPathRadius,
                centerX + mPathRadius,
                centerY + mPathRadius,
                textStartAngle,
                clockwiseFactor * mTextSweepDegrees
        );

        if (withBackground) {
            mBgPath.reset();
            float radius1 = mPathRadius - clockwiseFactor * mPaint.getFontMetrics().descent;
            float radius2 = mPathRadius - clockwiseFactor * mPaint.getFontMetrics().ascent;
            mBgPath.arcTo(
                    centerX - radius2,
                    centerY - radius2,
                    centerX + radius2,
                    centerY + radius2,
                    backgroundStartAngle,
                    clockwiseFactor * mBackgroundSweepDegrees, false
            );
            mBgPath.arcTo(
                    centerX - radius1,
                    centerY - radius1,
                    centerX + radius1,
                    centerY + radius1,
                    backgroundStartAngle + clockwiseFactor * mBackgroundSweepDegrees,
                    -clockwiseFactor * mBackgroundSweepDegrees, false
            );
            mBgPath.close();

            float angle = backgroundStartAngle;
            float x0 = (float) (centerX + radius2 * cos(angle * Math.PI / 180));
            float x1 = (float) (centerX + radius1 * cos(angle * Math.PI / 180));
            float y0 = (float) (centerX + radius2 * sin(angle * Math.PI / 180));
            float y1 = (float) (centerX + radius1 * sin(angle * Math.PI / 180));
            angle = backgroundStartAngle + clockwiseFactor * mBackgroundSweepDegrees;
            float x2 = (float) (centerX + radius2 * cos(angle * Math.PI / 180));
            float x3 = (float) (centerX + radius1 * cos(angle * Math.PI / 180));
            // Background axis-aligned bounding box calculation. Note that, we always center the
            // text on the top-center of the view.
            // top: always will be centerY - outerRadius
            // bottom: the max y of end points of the outer and inner arc contains the text
            // left: if over -90 degrees, centerX - outerRadius, otherwise the min x of start,
            // end points of the outer and inner arc contains the text
            // right: if over 90 degrees, centerX + outerRadius, otherwise the max x of start,
            // end points of the outer and inner arc contains the text
            float outerRadius = max(radius1, radius2);
            mBgBounds.top = (int) (centerY - outerRadius);
            mBgBounds.bottom = (int) max(y0, y1);
            mBgBounds.left =
                    mBackgroundSweepDegrees >= 180.0f
                            ? (int) (centerX - outerRadius)
                            : (int) min(x0, min(x1, min(x2, x3)));
            mBgBounds.right =
                    mBackgroundSweepDegrees >= 180.0f
                            ? (int) (centerX + outerRadius)
                            : (int) max(x0, max(x1, max(x2, x3)));
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        canvas.save();

        boolean withBackground = getBackground() != null;
        updatePathsIfNeeded(withBackground);
        canvas.rotate(
                mLocalRotateAngle + mParentRotateAngle,
                getWidth() / 2f,
                getHeight() / 2f);

        if (withBackground) {
            canvas.clipPath(mBgPath);
            getBackground().setBounds(mBgBounds);
        }
        super.draw(canvas);

        canvas.restore();
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        mPaint.setColor(mTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawTextOnPath(mTextToDraw, mPath, 0f, 0f, mPaint);
    }

    /**
     * Sets the Typeface taking into account the given attributes.
     *
     * @param familyName    family name string, e.g. "serif"
     * @param typefaceIndex an index of the typeface enum, e.g. SANS, SERIF.
     * @param style         a typeface style
     * @param weight        a weight value for the Typeface or -1 if not specified.
     */
    private void setTypefaceFromAttrs(
            @Nullable String familyName,
            int typefaceIndex,
            int style,
            int weight
    ) {
        // typeface is ignored when font family is set
        if (mTypeface == null && familyName != null) {
            // Lookup normal Typeface from system font map.
            Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
            resolveStyleAndSetTypeface(normalTypeface, style, weight);
        } else if (mTypeface != null) {
            resolveStyleAndSetTypeface(mTypeface, style, weight);
        } else { // both typeface and familyName is null.
            switch (typefaceIndex) {
                case 1:
                    resolveStyleAndSetTypeface(Typeface.SANS_SERIF, style, weight);
                    break;
                case 2:
                    resolveStyleAndSetTypeface(Typeface.SERIF, style, weight);
                    break;
                case 3:
                    resolveStyleAndSetTypeface(Typeface.MONOSPACE, style, weight);
                    break;
                default:
                    resolveStyleAndSetTypeface(null, style, weight);
            }
        }
    }

    private void resolveStyleAndSetTypeface(@Nullable Typeface tf, int style, int weight) {
        if (weight >= 0 && Build.VERSION.SDK_INT >= 28) {
            int clampedWeight = min(FONT_WEIGHT_MAX, weight);
            boolean italic = (style & Typeface.ITALIC) != 0;
            mTypeface = Api28Impl.createTypeface(tf, clampedWeight, italic);
            mPaint.setTypeface(mTypeface);
        } else {
            setTypeface(tf, style);
        }
    }

    /**
     * Sets the typeface and style in which the text should be displayed, and turns on the fake
     * bold and italic bits in the Paint if the Typeface that you provided does not have all the
     * bits in the style that you specified.
     */
    private void setTypeface(@Nullable Typeface tf, int style) {
        if (style > 0) {
            if (tf == null) {
                tf = Typeface.defaultFromStyle(style);
            } else {
                tf = Typeface.create(tf, style);
            }
            if (!tf.equals(mPaint.getTypeface())) {
                mPaint.setTypeface(tf);
                mTypeface = tf;
            }
            // now compute what (if any) algorithmic styling is needed
            int typefaceStyle = tf != null ? tf.getStyle() : 0;
            int need = style & ~typefaceStyle;
            mPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
            mPaint.setTextSkewX(((need & Typeface.ITALIC) != 0) ? ITALIC_SKEW_X : 0f);
        } else {
            mPaint.setFakeBoldText(false);
            mPaint.setTextSkewX(0f);
            if ((tf != null && !tf.equals(mPaint.getTypeface()))
                    || (tf == null && mPaint.getTypeface() != null)) {
                mPaint.setTypeface(tf);
                mTypeface = tf;
            }
        }
    }

    /**
     * Set of attribute that can be defined in a Text Appearance.
     */
    private static class TextAppearanceAttributes {
        @Nullable
        ColorStateList mTextColor = null;
        float mTextSize = DEFAULT_TEXT_SIZE;
        @Nullable
        String mFontFamily = null;
        boolean mFontFamilyExplicit = false;
        int mTypefaceIndex = -1;
        int mTextStyle = DEFAULT_TEXT_STYLE;
        int mFontWeight = -1;
        float mLetterSpacing = 0f;
        @Nullable
        String mFontFeatureSettings = null;
        @Nullable
        String mFontVariationSettings = null;

        TextAppearanceAttributes() {
        }
    }

    /**
     * Sets the textColor, size, style, font etc from the specified TextAppearanceAttributes
     */
    private void applyTextAppearance(TextAppearanceAttributes attributes) {
        if (attributes.mTextColor != null) {
            mTextColor = attributes.mTextColor.getDefaultColor();
        }

        if (attributes.mTextSize != -1f) {
            mTextSize = attributes.mTextSize;
        }

        setTypefaceFromAttrs(
                attributes.mFontFamily,
                attributes.mTypefaceIndex,
                attributes.mTextStyle,
                attributes.mFontWeight
        );

        mPaint.setLetterSpacing(attributes.mLetterSpacing);
        mLetterSpacing = attributes.mLetterSpacing;
        mPaint.setFontFeatureSettings(attributes.mFontFeatureSettings);
        mFontFeatureSettings = attributes.mFontFeatureSettings;
        if (Build.VERSION.SDK_INT >= 26) {
            Api26Impl.paintSetFontVariationSettings(mPaint, attributes.mFontVariationSettings);
        }
        mFontVariationSettings = attributes.mFontVariationSettings;
    }

    /**
     * Read the Text Appearance attributes from a given TypedArray and set its values to the
     * given set. If the TypedArray contains a value that already set in the given attributes,
     * that will be overridden.
     */
    private void readTextAppearance(
            TypedArray appearance,
            TextAppearanceAttributes attributes,
            boolean isTextAppearance
    ) {
        int attrIndex = isTextAppearance ? R.styleable.TextAppearance_android_textColor :
                R.styleable.WearCurvedTextView_android_textColor;
        if (appearance.hasValue(attrIndex)) {
            attributes.mTextColor = appearance.getColorStateList(attrIndex);
        }

        attributes.mTextSize = appearance.getDimensionPixelSize(
                isTextAppearance ? R.styleable.TextAppearance_android_textSize :
                        R.styleable.WearCurvedTextView_android_textSize,
                (int) attributes.mTextSize
        );

        attributes.mTextStyle = appearance.getInt(
                isTextAppearance ? R.styleable.TextAppearance_android_textStyle :
                        R.styleable.WearCurvedTextView_android_textStyle,
                attributes.mTextStyle
        );

        // make sure that the typeface attribute is read before fontFamily attribute
        attributes.mTypefaceIndex = appearance.getInt(
                isTextAppearance ? R.styleable.TextAppearance_android_typeface :
                        R.styleable.WearCurvedTextView_android_typeface,
                attributes.mTypefaceIndex
        );
        if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
            attributes.mFontFamily = null;
        }

        attrIndex = isTextAppearance ? R.styleable.TextAppearance_android_fontFamily :
                R.styleable.WearCurvedTextView_android_fontFamily;
        if (appearance.hasValue(attrIndex)) {
            attributes.mFontFamily = appearance.getString(attrIndex);
            attributes.mFontFamilyExplicit = !isTextAppearance;
        }

        attributes.mFontWeight = appearance.getInt(
                isTextAppearance ? R.styleable.TextAppearance_android_textFontWeight :
                        R.styleable.WearCurvedTextView_android_textFontWeight,
                attributes.mFontWeight
        );

        attributes.mLetterSpacing = appearance.getFloat(
                isTextAppearance ? R.styleable.TextAppearance_android_letterSpacing :
                        R.styleable.WearCurvedTextView_android_letterSpacing,
                attributes.mLetterSpacing
        );

        attrIndex = isTextAppearance ? R.styleable.TextAppearance_android_fontFeatureSettings :
                R.styleable.WearCurvedTextView_android_fontFeatureSettings;
        if (appearance.hasValue(attrIndex)) {
            attributes.mFontFeatureSettings = appearance.getString(attrIndex);
        }

        attrIndex = isTextAppearance ? R.styleable.TextAppearance_android_fontVariationSettings :
                R.styleable.WearCurvedTextView_android_fontVariationSettings;
        if (appearance.hasValue(attrIndex)) {
            attributes.mFontVariationSettings = appearance.getString(attrIndex);
        }
    }

    private void doUpdate() {
        mDirty = true;
        updatePaint();
        requestLayout();
        postInvalidate();
    }

    private void doRedraw() {
        mDirty = true;
        postInvalidate();
    }

    /** returns the anchor type for positioning the curved text */
    public int getAnchorType() {
        return mAnchorType;
    }

    /**
     * Sets the anchor type for positioning the curved text.
     * @param value the anchor type,  one of {ANCHOR_START, ANCHOR_CENTER, ANCHOR_END}
     */
    public void setAnchorType(@WearArcLayout.AnchorType int value) {
        mAnchorType = value;
        doUpdate();
    }

    /** Returns the anchor angle used for positioning the text, in degrees. */
    public float getAnchorAngleDegrees() {
        return mAnchorAngleDegrees;
    }

    /** Sets the anchor angle used for positioning the text, in degrees. */
    public void setAnchorAngleDegrees(float value) {
        mAnchorAngleDegrees = value;
        doRedraw();
    }

    /** returns the sweep angle in degrees for rendering the text */
    public float getSweepDegrees() {
        return mSweepDegrees;
    }

    /** sets the sweep angle in degrees for rendering the text */
    public void setSweepDegrees(float value) {
        mSweepDegrees = value;
        doUpdate();
    }

    /**  returns the text to be rendered */
    @NonNull
    public String getText() {
        return mText;
    }

    /** sets the text to be rendered */
    public void setText(@NonNull String value) {
        mText = value;
        doUpdate();
    }

    /** returns the text size for rendering the text */
    public float getTextSize() {
        return mTextSize;
    }

    /** sets the text size for rendering the text */
    public void setTextSize(float value) {
        mTextSize = value;
        doUpdate();
    }

    /** Gets the current Typeface that is used to style the text. */
    @Nullable
    public Typeface getTypeface() {
        return mTypeface;
    }

    /**
     * Sets the typeface and style in which the text should be displayed. Note that not all
     * Typeface families actually have bold and italic variants
     */
    public void setTypeface(@Nullable Typeface value) {
        mTypeface = value;
        doUpdate();
    }

    /** returns the curved text layout direction */
    public boolean getClockwise() {
        return mClockwise;
    }

    /** sets the curved text layout direction */
    public void setClockwise(boolean value) {
        mClockwise = value;
        doUpdate();
    }

    /** returns the color for rendering the text */
    @ColorInt
    public int getTextColor() {
        return mTextColor;
    }

    /** sets the color for rendering the text */
    public void setTextColor(@ColorInt int value) {
        mTextColor = value;
        doRedraw();
    }

    /**
     * Returns where, if anywhere, words that are longer than the view is wide should be
     * ellipsized.
     */
    @Nullable
    public TextUtils.TruncateAt getEllipsize() {
        return mEllipsize;
    }

    /**
     * Causes words in the text that are longer than the view's width to be ellipsized. Use null
     * to turn off ellipsizing.
     */
    public void setEllipsize(@Nullable TextUtils.TruncateAt value) {
        mEllipsize = value;
        doRedraw();
    }

    /**
     * Gets the text letter-space value, which determines the spacing between characters. The
     * value returned is in ems. Normally, this value is 0.0.
     * @return The text letter-space value in ems.
     */
    public float getLetterSpacing() {
        return mLetterSpacing;
    }

    /**
     * Sets text letter-spacing in ems. Typical values for slight expansion will be around 0.05.
     * Negative values tighten text.
     * @param value A text letter-space value in ems.
     */
    public void setLetterSpacing(float value) {
        mLetterSpacing = value;
        doUpdate();
    }

    /**
     * Returns the font feature settings. The format is the same as the CSS font-feature-settings
     * attribute: https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
     * @return the currently set font feature settings. Default is null.
     */
    @Nullable
    public String getFontFeatureSettings() {
        return mFontFeatureSettings;
    }

    /**
     * Sets font feature settings. The format is the same as the CSS font-feature-settings
     * attribute: https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
     * @param value font feature settings represented as CSS compatible string. This value may be
     *             null.
     */
    public void setFontFeatureSettings(@Nullable String value) {
        mFontFeatureSettings = value;
        doUpdate();
    }

    /** Returns TrueType or OpenType font variation settings. */
    @Nullable
    public String getFontVariationSettings() {
        return mFontVariationSettings;
    }

    /**
     * Sets TrueType or OpenType font variation settings.
     * @param value font variation settings. You can pass null or empty string as no variation
     *              settings. This value may be null
     */
    public void setFontVariationSettings(@Nullable String value) {
        mFontVariationSettings = value;
        doUpdate();
    }

    /**
     * Nested class to avoid verification errors for methods induces in API level 26
     */
    @RequiresApi(26)
    private static class Api26Impl {
        private Api26Impl() {
        }

        static void paintSetFontVariationSettings(
                Paint paint,
                @Nullable String fontVariationSettings) {
            paint.setFontVariationSettings(fontVariationSettings);
        }
    }

    /**
     * Nested class to avoid verification errors for methods induces in API level 28
     */
    @RequiresApi(28)
    private static class Api28Impl {
        private Api28Impl() {
        }

        static Typeface createTypeface(@Nullable Typeface family, int weight, boolean italic) {
            return Typeface.create(family, weight, italic);
        }
    }

}