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
Methods |
---|
public void | checkInvalidAttributeAsChild()
|
public void | draw(Canvas canvas)
|
public float | getAnchorAngleDegrees()
Returns the anchor angle used for positioning the text, in degrees. |
public int | getAnchorType()
Returns the anchor type for positioning the curved text |
public TextUtils.TruncateAt | getEllipsize()
Returns where, if anywhere, words that are longer than the view is wide should be
ellipsized. |
public java.lang.String | getFontFeatureSettings()
Returns the font feature settings. |
public java.lang.String | getFontVariationSettings()
Returns TrueType or OpenType font variation settings. |
public float | getLetterSpacing()
Gets the text letter-space value, which determines the spacing between characters. |
public float | getMaxSweepDegrees()
Returns the maximum sweep angle in degrees for rendering the text |
public float | getMinSweepDegrees()
Returns the sweep angle in degrees for rendering the text |
public float | getSweepAngleDegrees()
|
public java.lang.String | getText()
Returns the text to be rendered |
public int | getTextColor()
Returns the color for rendering the text |
public float | getTextSize()
Returns the text size for rendering the text |
public int | getThickness()
|
public Typeface | getTypeface()
Gets the current Typeface that is used to style the text. |
public boolean | isClockwise()
Returns the curved text layout direction |
public boolean | isPointInsideClickArea(float x, float y)
See ArcLayout.Widget.isPointInsideClickArea(float, float) |
protected void | onDraw(Canvas canvas)
|
public void | onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
|
protected void | onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
public void | onPopulateAccessibilityEvent(AccessibilityEvent event)
|
protected void | onSizeChanged(int w, int h, int oldw, int oldh)
|
public boolean | onTouchEvent(MotionEvent event)
|
public void | setAnchorAngleDegrees(float value)
Sets the anchor angle used for positioning the text, in degrees. |
public void | setAnchorType(int value)
Sets the anchor type for positioning the curved text. |
public void | setClockwise(boolean value)
Sets the curved text layout direction |
public void | setEllipsize(TextUtils.TruncateAt value)
Causes words in the text that are longer than the view's width to be ellipsized. |
public void | setFontFeatureSettings(java.lang.String value)
Sets font feature settings. |
public void | setFontVariationSettings(java.lang.String value)
Sets TrueType or OpenType font variation settings. |
public void | setLetterSpacing(float value)
Sets text letter-spacing in ems. |
public void | setSweepAngleDegrees(float angleDegrees)
|
public void | setSweepRangeDegrees(float minSweep, float maxSweep)
Sets the minimum and maximum sweep angle in degrees for rendering the text. |
public void | setText(java.lang.String value)
Sets the text to be rendered |
public void | setTextColor(int value)
Sets the color for rendering the text |
public void | setTextSize(float value)
Sets the text size for rendering the text |
public void | setTypeface(Typeface value)
Sets the typeface and style in which the text should be displayed. |
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. |
from java.lang.Object | clone, 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);
}
}
}