public class

CurvedTextView

extends View

implements ArcLayout.Widget

 java.lang.Object

↳View

↳androidx.wear.widget.CurvedTextView

Gradle dependencies

compile group: 'androidx.wear', name: 'wear', version: '1.4.0-alpha01'

  • groupId: androidx.wear
  • artifactId: wear
  • version: 1.4.0-alpha01

Artifact androidx.wear:wear:1.4.0-alpha01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.wear:wear com.android.support:wear

Overview

CurvedTextView is a component allowing developers to easily write curved text following the curvature of the largest circle that can be inscribed in the view. ArcLayout could be used to concatenate multiple curved texts, also layout together with other widgets such as icons.

Summary

Constructors
publicCurvedTextView(Context context)

publicCurvedTextView(Context context, AttributeSet attrs)

publicCurvedTextView(Context context, AttributeSet attrs, int defStyle)

publicCurvedTextView(Context context, AttributeSet attrs, int defStyle, int defStyleRes)

Methods
public voidcheckInvalidAttributeAsChild()

public voiddraw(Canvas canvas)

public floatgetAnchorAngleDegrees()

Returns the anchor angle used for positioning the text, in degrees.

public intgetAnchorType()

Returns the anchor type for positioning the curved text

public TextUtils.TruncateAtgetEllipsize()

Returns where, if anywhere, words that are longer than the view is wide should be ellipsized.

public java.lang.StringgetFontFeatureSettings()

Returns the font feature settings.

public java.lang.StringgetFontVariationSettings()

Returns TrueType or OpenType font variation settings.

public floatgetLetterSpacing()

Gets the text letter-space value, which determines the spacing between characters.

public floatgetMaxSweepDegrees()

Returns the maximum sweep angle in degrees for rendering the text

public floatgetMinSweepDegrees()

Returns the sweep angle in degrees for rendering the text

public floatgetSweepAngleDegrees()

public java.lang.StringgetText()

Returns the text to be rendered

public intgetTextColor()

Returns the color for rendering the text

public floatgetTextSize()

Returns the text size for rendering the text

public intgetThickness()

public TypefacegetTypeface()

Gets the current Typeface that is used to style the text.

public booleanisClockwise()

Returns the curved text layout direction

public booleanisPointInsideClickArea(float x, float y)

See ArcLayout.Widget.isPointInsideClickArea(float, float)

protected voidonDraw(Canvas canvas)

public voidonInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)

protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

public voidonPopulateAccessibilityEvent(AccessibilityEvent event)

protected voidonSizeChanged(int w, int h, int oldw, int oldh)

public booleanonTouchEvent(MotionEvent event)

public voidsetAnchorAngleDegrees(float value)

Sets the anchor angle used for positioning the text, in degrees.

public voidsetAnchorType(int value)

Sets the anchor type for positioning the curved text.

public voidsetClockwise(boolean value)

Sets the curved text layout direction

public voidsetEllipsize(TextUtils.TruncateAt value)

Causes words in the text that are longer than the view's width to be ellipsized.

public voidsetFontFeatureSettings(java.lang.String value)

Sets font feature settings.

public voidsetFontVariationSettings(java.lang.String value)

Sets TrueType or OpenType font variation settings.

public voidsetLetterSpacing(float value)

Sets text letter-spacing in ems.

public voidsetSweepAngleDegrees(float angleDegrees)

public voidsetSweepRangeDegrees(float minSweep, float maxSweep)

Sets the minimum and maximum sweep angle in degrees for rendering the text.

public voidsetText(java.lang.String value)

Sets the text to be rendered

public voidsetTextColor(int value)

Sets the color for rendering the text

public voidsetTextSize(float value)

Sets the text size for rendering the text

public voidsetTypeface(Typeface value)

Sets the typeface and style in which the text should be displayed.

public voidsetTypeface(Typeface tf, int 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.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public CurvedTextView(Context context)

public CurvedTextView(Context context, AttributeSet attrs)

public CurvedTextView(Context context, AttributeSet attrs, int defStyle)

public CurvedTextView(Context context, AttributeSet attrs, int defStyle, int defStyleRes)

Methods

public float getSweepAngleDegrees()

public void setSweepAngleDegrees(float angleDegrees)

public int getThickness()

public void checkInvalidAttributeAsChild()

public boolean isPointInsideClickArea(float x, float y)

See ArcLayout.Widget.isPointInsideClickArea(float, float)

protected void onSizeChanged(int w, int h, int oldw, int oldh)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

public boolean onTouchEvent(MotionEvent event)

public void draw(Canvas canvas)

protected void onDraw(Canvas canvas)

public void setTypeface(Typeface tf, int 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.

public int getAnchorType()

Returns the anchor type for positioning the curved text

public void setAnchorType(int value)

Sets the anchor type for positioning the curved text.

Parameters:

value: the anchor type, one of {ANCHOR_START, ANCHOR_CENTER, ANCHOR_END}

public float getAnchorAngleDegrees()

Returns the anchor angle used for positioning the text, in degrees.

public void setAnchorAngleDegrees(float value)

Sets the anchor angle used for positioning the text, in degrees.

public void setSweepRangeDegrees(float minSweep, float maxSweep)

Sets the minimum and maximum sweep angle in degrees for rendering the text.

Parameters:

minSweep: Ensure the text takes at least this angle (in degrees) in the arc. Use 0f if you don't want to specify a minimum.
maxSweep: Limit the maximum angle (in degrees) that this curved text can take. Use 360f if you don't want to specify a maximum.

public float getMinSweepDegrees()

Returns the sweep angle in degrees for rendering the text

public float getMaxSweepDegrees()

Returns the maximum sweep angle in degrees for rendering the text

public java.lang.String getText()

Returns the text to be rendered

public void setText(java.lang.String value)

Sets the text to be rendered

public float getTextSize()

Returns the text size for rendering the text

public void setTextSize(float value)

Sets the text size for rendering the text

public Typeface getTypeface()

Gets the current Typeface that is used to style the text.

public void setTypeface(Typeface value)

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 boolean isClockwise()

Returns the curved text layout direction

public void setClockwise(boolean value)

Sets the curved text layout direction

public int getTextColor()

Returns the color for rendering the text

public void setTextColor(int value)

Sets the color for rendering the text

public TextUtils.TruncateAt getEllipsize()

Returns where, if anywhere, words that are longer than the view is wide should be ellipsized.

public void setEllipsize(TextUtils.TruncateAt value)

Causes words in the text that are longer than the view's width to be ellipsized. Use null to turn off ellipsizing.

public float getLetterSpacing()

Gets the text letter-space value, which determines the spacing between characters. The value returned is in ems. Normally, this value is 0.0.

Returns:

The text letter-space value in ems.

public void setLetterSpacing(float value)

Sets text letter-spacing in ems. Typical values for slight expansion will be around 0.05. Negative values tighten text.

Parameters:

value: A text letter-space value in ems.

public java.lang.String getFontFeatureSettings()

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

Returns:

The currently set font feature settings. Default is null.

public void setFontFeatureSettings(java.lang.String value)

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

Parameters:

value: Font feature settings represented as CSS compatible string. This value may be null.

public java.lang.String getFontVariationSettings()

Returns TrueType or OpenType font variation settings.

public void setFontVariationSettings(java.lang.String value)

Sets TrueType or OpenType font variation settings.

Parameters:

value: Font variation settings. You can pass null or empty string as no variation settings. This value may be null

public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)

public void onPopulateAccessibilityEvent(AccessibilityEvent event)

Source

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

package androidx.wear.widget;

import static java.lang.Math.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.annotation.SuppressLint;
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.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

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

/**
 * CurvedTextView is a component allowing developers to easily write curved text following
 * the curvature of the largest circle that can be inscribed in the view. ArcLayout could be
 * used to concatenate multiple curved texts, also layout together with other widgets such as icons.
 */
public class CurvedTextView extends View implements ArcLayout.Widget {
    private static final float UNSET_ANCHOR_DEGREE = -1f;
    private static final int UNSET_ANCHOR_TYPE = -1;
    private static final float MIN_SWEEP_DEGREE = 0f;
    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 int mLastUsedTextAlignment = -1;
    private float mLocalRotateAngle = 0f;

    @ArcLayout.AnchorType
    private int mAnchorType;
    private float mAnchorAngleDegrees;
    private float mMinSweepDegrees;
    private float mMaxSweepDegrees;
    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;

    // If true, it means we got the touch_down event and are receiving the touch events that follow.
    private boolean mHandlingTouch = false;


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

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

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

    public CurvedTextView(
            @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.CurvedTextView, 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.CurvedTextView_android_text)) {
            mText = a.getString(R.styleable.CurvedTextView_android_text);
        }

        int textEllipsize = a.getInt(R.styleable.CurvedTextView_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 CurvedTextView attributes
        mMaxSweepDegrees =
                a.getFloat(R.styleable.CurvedTextView_maxSweepDegrees, MAX_SWEEP_DEGREE);
        mMaxSweepDegrees = min(mMaxSweepDegrees, MAX_SWEEP_DEGREE);
        mMinSweepDegrees =
                a.getFloat(R.styleable.CurvedTextView_minSweepDegrees, MIN_SWEEP_DEGREE);
        if (mMinSweepDegrees > mMaxSweepDegrees) {
            throw new IllegalArgumentException(
                    "MinSweepDegrees cannot be bigger than MaxSweepDegrees"
            );
        }
        mAnchorType = a.getInt(R.styleable.CurvedTextView_anchorPosition, UNSET_ANCHOR_TYPE);
        mAnchorAngleDegrees = a.getFloat(
                R.styleable.CurvedTextView_anchorAngleDegrees, UNSET_ANCHOR_DEGREE
        );
        mAnchorAngleDegrees = mAnchorAngleDegrees % 360f;
        mClockwise = a.getBoolean(R.styleable.CurvedTextView_clockwise, DEFAULT_CLOCKWISE);

        a.recycle();

        applyTextAppearance(attributes);

        mPaint.setTextSize(mTextSize);
    }

    @Override
    @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true)
    public float getSweepAngleDegrees() {
        return mBackgroundSweepDegrees;
    }

    @Override
    public void setSweepAngleDegrees(
            @FloatRange(from = 0.0f, to = 360.0f, toInclusive = true) float angleDegrees) {
        /** We need to be careful because this is also set by {@link #onMeasure} below. */
        mBackgroundSweepDegrees = angleDegrees;
    }

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

    /**
     * @throws IllegalArgumentException if the anchorType and/or anchorAngleDegrees attributes
     *                                  were set for a widget in ArcLayout
     */
    @Override
    public void checkInvalidAttributeAsChild() {
        if (mAnchorType != UNSET_ANCHOR_TYPE) {
            throw new IllegalArgumentException(
                    "CurvedTextView shall not set anchorType value when added into"
                            + "ArcLayout"
            );
        }

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

    /**
     * See {@link ArcLayout.Widget#isPointInsideClickArea(float, float)}
     */
    @Override
    public boolean isPointInsideClickArea(float x, float y) {
        float radius2 = min(getWidth(), getHeight()) / 2f
                - (mClockwise ? getPaddingTop() : getPaddingBottom());
        float radius1 =
                radius2 - mPaint.getFontMetrics().descent + mPaint.getFontMetrics().ascent;

        float dx = x - getWidth() / 2;
        float dy = y - getHeight() / 2;

        float r2 = dx * dx + dy * dy;
        if (r2 < radius1 * radius1 || r2 > radius2 * radius2) {
            return false;
        }

        // Since we are symmetrical on the Y-axis, we can constrain the angle to the x>=0 quadrants.
        float angle = (float) Math.toDegrees(Math.atan2(Math.abs(dx), -dy));
        return angle < mBackgroundSweepDegrees / 2;
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mPaint.getTextBounds(mText, 0, mText.length(), mBounds);

        // Note that ascent is negative.
        mPathRadius = min(getMeasuredWidth(), getMeasuredHeight()) / 2f
                + (mClockwise ? mPaint.getFontMetrics().ascent - getPaddingTop() :
                -mPaint.getFontMetrics().descent - getPaddingBottom());
        mTextSweepDegrees = min(
                getWidthSelf() / mPathRadius / (float) Math.PI * 180f,
                MAX_SWEEP_DEGREE);
        mBackgroundSweepDegrees = max(min(mMaxSweepDegrees, mTextSweepDegrees), mMinSweepDegrees);
    }

    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();

        if (mTextSweepDegrees <= mMaxSweepDegrees) {
            mTextToDraw = mText;
        } else {
            mTextToDraw = ellipsize(
                    (int) (mMaxSweepDegrees / 180f * Math.PI * mPathRadius) - getPaddingLeft()
                            - getPaddingRight()
            );
            mTextSweepDegrees = mMaxSweepDegrees;
        }

        float clockwiseFactor = mClockwise ? 1f : -1f;

        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;
        switch (mAnchorType) {
            case ArcLayout.ANCHOR_START:
                anchorTypeFactor = 0.5f;
                break;
            case ArcLayout.ANCHOR_END:
                anchorTypeFactor = -0.5f;
                break;
            case ArcLayout.ANCHOR_CENTER: // Center is the default.
            default:
                anchorTypeFactor = 0f;
        }

        mLocalRotateAngle = (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
                + clockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;

        // 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;

        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();
            // NOTE: Ensure that if the code to compute these radius* change, containsPoint() is
            // also updated.
            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) (centerY + radius2 * sin(angle * Math.PI / 180));
            float y1 = (float) (centerY + 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
    // We only filter events and defer to super.onTouchEvent()
    @SuppressLint("ClickableViewAccessibility")
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (!mHandlingTouch && event.getAction() != MotionEvent.ACTION_DOWN) {
            return false;
        }

        float x0 = event.getX() - getWidth() / 2;
        float y0 = event.getY() - getHeight() / 2;

        double rotAngle = -Math.toRadians(mLocalRotateAngle);

        float tempX = (float)
                ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
        y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
        x0 = tempX;

        // Should we start handling the touch events?
        if (!mHandlingTouch && isPointInsideClickArea(x0, y0)) {
            mHandlingTouch = true;
        }

        // We just started or are in the middle of handling events, forward to View to handle.
        if (mHandlingTouch) {
            if (event.getAction() == MotionEvent.ACTION_UP
                    || event.getAction() == MotionEvent.ACTION_CANCEL) {
                // We should end handling events now
                mHandlingTouch = false;
            }
            event.offsetLocation(x0 - event.getX(), y0 - event.getY());
            return super.onTouchEvent(event);
        }

        return false;
    }

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

        boolean withBackground = getBackground() != null;
        updatePathsIfNeeded(withBackground);
        canvas.rotate(
                mLocalRotateAngle,
                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.
     */
    public 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;
            }
        }
        doUpdate();
    }

    /**
     * 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.CurvedTextView_android_textColor;
        if (appearance.hasValue(attrIndex)) {
            attributes.mTextColor = appearance.getColorStateList(attrIndex);
        }

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

        attributes.mTextStyle = appearance.getInt(
                isTextAppearance ? R.styleable.TextAppearance_android_textStyle :
                        R.styleable.CurvedTextView_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.CurvedTextView_android_typeface,
                attributes.mTypefaceIndex
        );
        if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
            attributes.mFontFamily = null;
        }

        attrIndex = isTextAppearance ? R.styleable.TextAppearance_android_fontFamily :
                R.styleable.CurvedTextView_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.CurvedTextView_android_textFontWeight,
                attributes.mFontWeight
        );

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

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

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

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

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

    /** Returns the anchor type for positioning the curved text */
    @ArcLayout.AnchorType
    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(@ArcLayout.AnchorType int value) {
        mAnchorType = value;
        doUpdate();
    }

    /** Returns the anchor angle used for positioning the text, in degrees. */
    @FloatRange(from = 0f, to = 360f, toInclusive = true)
    public float getAnchorAngleDegrees() {
        return mAnchorAngleDegrees;
    }

    /** Sets the anchor angle used for positioning the text, in degrees. */
    public void setAnchorAngleDegrees(
            @FloatRange(from = 0f, to = 360f, toInclusive = true) float value) {
        mAnchorAngleDegrees = value;
        doRedraw();
    }

    /**
     * Sets the minimum and maximum sweep angle in degrees for rendering the text.
     *
     * @param minSweep Ensure the text takes at least this angle (in degrees) in the arc. Use 0f if
     *                 you don't want to specify a minimum.
     * @param maxSweep Limit the maximum angle (in degrees) that this curved text can take. Use
     *                 360f if you don't want to specify a maximum.
     */
    public void setSweepRangeDegrees(
            @FloatRange(from = 0f, to = 360f, toInclusive = true) float minSweep,
            @FloatRange(from = 0f, to = 360f, toInclusive = true) float maxSweep) {
        if (minSweep > maxSweep) {
            throw new IllegalArgumentException(
                    "MaxSweepDegrees cannot be smaller than MinSweepDegrees"
            );
        }
        mMinSweepDegrees = min(max(minSweep, MIN_SWEEP_DEGREE), MAX_SWEEP_DEGREE);
        mMaxSweepDegrees = min(maxSweep, MAX_SWEEP_DEGREE);
        doUpdate();
    }

    /** Returns the sweep angle in degrees for rendering the text */
    @FloatRange(from = 0f, to = 360f, toInclusive = true)
    public float getMinSweepDegrees() {
        return mMinSweepDegrees;
    }

    /** Returns the maximum sweep angle in degrees for rendering the text */
    @FloatRange(from = 0f, to = 360f, toInclusive = true)
    public float getMaxSweepDegrees() {
        return mMaxSweepDegrees;
    }

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

    /** Sets the text to be rendered */
    public void setText(@Nullable String value) {
        mText = value == null ? "" : 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;
        mPaint.setTextSize(mTextSize);
        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 isClockwise() {
        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();
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setText(mText);
    }

    @Override
    public void onPopulateAccessibilityEvent(@NonNull AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(event);
        event.getText().add(mText);
    }

    /**
     * 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);
        }
    }

}