Android 中的垂直(旋转)标签

我需要在 Android 中显示垂直标签的两种方法:

  1. 水平标签转90度逆时针方向(旁边的字母)
  2. 横向标签,字母一个接一个(像商店标志)

我是否需要为这两种情况(一种情况)开发自定义小部件,我是否可以让 TextView 以这种方式呈现,如果我需要完全自定义,有什么好方法可以做到这一点?

86215 次浏览

实现这些目标的一种方法是:

  1. 编写自己的自定义 view and override onDraw(Canvas).您可以在画布上绘制文本,然后旋转画布。
  2. 与1相同,只不过这次使用 路径并使用 DrawTextOnPath (...)绘制文本

我为我的 ChartDroid项目实现了这个。创建 VerticalLabelView.java:

public class VerticalLabelView extends View {
private TextPaint mTextPaint;
private String mText;
private int mAscent;
private Rect text_bounds = new Rect();


final static int DEFAULT_TEXT_SIZE = 15;


public VerticalLabelView(Context context) {
super(context);
initLabelView();
}


public VerticalLabelView(Context context, AttributeSet attrs) {
super(context, attrs);
initLabelView();


TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VerticalLabelView);


CharSequence s = a.getString(R.styleable.VerticalLabelView_text);
if (s != null) setText(s.toString());


setTextColor(a.getColor(R.styleable.VerticalLabelView_textColor, 0xFF000000));


int textSize = a.getDimensionPixelOffset(R.styleable.VerticalLabelView_textSize, 0);
if (textSize > 0) setTextSize(textSize);


a.recycle();
}


private final void initLabelView() {
mTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(DEFAULT_TEXT_SIZE);
mTextPaint.setColor(0xFF000000);
mTextPaint.setTextAlign(Align.CENTER);
setPadding(3, 3, 3, 3);
}


public void setText(String text) {
mText = text;
requestLayout();
invalidate();
}


public void setTextSize(int size) {
mTextPaint.setTextSize(size);
requestLayout();
invalidate();
}


public void setTextColor(int color) {
mTextPaint.setColor(color);
invalidate();
}


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


mTextPaint.getTextBounds(mText, 0, mText.length(), text_bounds);
setMeasuredDimension(
measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}


private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);


if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
result = text_bounds.height() + getPaddingLeft() + getPaddingRight();


if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}


private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);


mAscent = (int) mTextPaint.ascent();
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
result = text_bounds.width() + getPaddingTop() + getPaddingBottom();


if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);


float text_horizontally_centered_origin_x = getPaddingLeft() + text_bounds.width()/2f;
float text_horizontally_centered_origin_y = getPaddingTop() - mAscent;


canvas.translate(text_horizontally_centered_origin_y, text_horizontally_centered_origin_x);
canvas.rotate(-90);
canvas.drawText(mText, 0, 0, mTextPaint);
}
}

And in attrs.xml:

<resources>
<declare-styleable name="VerticalLabelView">
<attr name="text" format="string" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
</declare-styleable>
</resources>

下面是我优雅而简单的垂直文本实现,它扩展了 TextView。这意味着可以使用 TextView 的所有标准样式,因为它是扩展的 TextView。

public class VerticalTextView extends TextView{
final boolean topDown;


public VerticalTextView(Context context, AttributeSet attrs){
super(context, attrs);
final int gravity = getGravity();
if(Gravity.isVertical(gravity) && (gravity&Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
setGravity((gravity&Gravity.HORIZONTAL_GRAVITY_MASK) | Gravity.TOP);
topDown = false;
}else
topDown = true;
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}


@Override
protected boolean setFrame(int l, int t, int r, int b){
return super.setFrame(l, t, l+(b-t), t+(r-l));
}


@Override
public void draw(Canvas canvas){
if(topDown){
canvas.translate(getHeight(), 0);
canvas.rotate(90);
}else {
canvas.translate(0, getWidth());
canvas.rotate(-90);
}
canvas.clipRect(0, 0, getWidth(), getHeight(), android.graphics.Region.Op.REPLACE);
super.draw(canvas);
}
}

默认情况下,旋转的文本是从上到下的。如果你设置 android: vity = “ bottom”,那么它就是从下到上绘制的。

从技术上讲,它愚弄了 TextView 底层,认为它是正常的旋转(在少数地方交换宽度/高度) ,而绘制它旋转。 在 xml 布局中使用它也可以很好地工作。

编辑: posting another version, above has problems with animations. This new version works better, but loses some TextView features, such as marquee and similar specialties.

public class VerticalTextView extends TextView{
final boolean topDown;


public VerticalTextView(Context context, AttributeSet attrs){
super(context, attrs);
final int gravity = getGravity();
if(Gravity.isVertical(gravity) && (gravity&Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
setGravity((gravity&Gravity.HORIZONTAL_GRAVITY_MASK) | Gravity.TOP);
topDown = false;
}else
topDown = true;
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}


@Override
protected void onDraw(Canvas canvas){
TextPaint textPaint = getPaint();
textPaint.setColor(getCurrentTextColor());
textPaint.drawableState = getDrawableState();


canvas.save();


if(topDown){
canvas.translate(getWidth(), 0);
canvas.rotate(90);
}else {
canvas.translate(0, getHeight());
canvas.rotate(-90);
}




canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop());


getLayout().draw(canvas);
canvas.restore();
}
}

EDIT 科特林版本:

import android.content.Context
import android.graphics.Canvas
import android.text.BoringLayout
import android.text.Layout
import android.text.TextUtils.TruncateAt
import android.util.AttributeSet
import android.view.Gravity
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.graphics.withSave


class VerticalTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) {
private val topDown = gravity.let { g ->
!(Gravity.isVertical(g) && g.and(Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM)
}
private val metrics = BoringLayout.Metrics()
private var padLeft = 0
private var padTop = 0


private var layout1: Layout? = null


override fun setText(text: CharSequence, type: BufferType) {
super.setText(text, type)
layout1 = null
}


private fun makeLayout(): Layout {
if (layout1 == null) {
metrics.width = height
paint.color = currentTextColor
paint.drawableState = drawableState
layout1 = BoringLayout.make(text, paint, metrics.width, Layout.Alignment.ALIGN_NORMAL, 2f, 0f, metrics, false, TruncateAt.END, height - compoundPaddingLeft - compoundPaddingRight)
padLeft = compoundPaddingLeft
padTop = extendedPaddingTop
}
return layout1!!
}


override fun onDraw(c: Canvas) {
//      c.drawColor(0xffffff80); // TEST
if (layout == null)
return
c.withSave {
if (topDown) {
val fm = paint.fontMetrics
translate(textSize - (fm.bottom + fm.descent), 0f)
rotate(90f)
} else {
translate(textSize, height.toFloat())
rotate(-90f)
}
translate(padLeft.toFloat(), padTop.toFloat())
makeLayout().draw(this)
}
}
}

还有一些小事情需要注意。

在选择旋转或路径方式时,它取决于字符集。例如,如果目标字符集是类似于英语的,并且预期的效果类似于,

a
b
c
d

您可以得到这种效果,绘制每个字符一个一个,没有旋转或路径需要。

enter image description here

你可能需要旋转或路径得到这种效果。

棘手的部分是当你试图渲染字符像蒙古语。Typeface 中的字形需要旋转90度,因此 draTextOnPath ()将是一个很好的选择。

check = (TextView)findViewById(R.id.check);
check.setRotation(-90);

这对我很有效,至于垂直向下的字母,我不知道。

根据 Pointer Null的回答,我可以通过这样修改 onDraw方法来使文本水平居中:

@Override
protected void onDraw(Canvas canvas){
TextPaint textPaint = getPaint();
textPaint.setColor(getCurrentTextColor());
textPaint.drawableState = getDrawableState();
canvas.save();
if(topDown){
canvas.translate(getWidth()/2, 0);
canvas.rotate(90);
}else{
TextView temp = new TextView(getContext());
temp.setText(this.getText().toString());
temp.setTypeface(this.getTypeface());
temp.measure(0, 0);
canvas.rotate(-90);
int max = -1 * ((getWidth() - temp.getMeasuredHeight())/2);
canvas.translate(canvas.getClipBounds().left, canvas.getClipBounds().top - max);
}
canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop());
getLayout().draw(canvas);
canvas.restore();
}

您可能需要添加 TextView memuredWidth 的一部分,以使多行文本居中。

在批准的答案中尝试了 VerticalTextView 类,它们工作得相当不错。

但是无论我怎么尝试,我都无法将这些垂直文本视图放置在包含布局的中心(一个 RelativeLayout,它是为回收视图膨胀的项目的一部分)。

FWIW, after looking around, I found yoog568's VerticalTextView class on GitHub:

Https://github.com/yoog568/verticaltextview/blob/master/src/com/yoog/widget/verticaltextview.java

您还需要在您的项目中包括以下属性定义:

Https://github.com/yoog568/verticaltextview/blob/master/res/values/attr.xml

我喜欢@kostmo 的方法。我稍作修改,因为我有一个问题-切断垂直旋转标签时,我设置它的参数为 WRAP_CONTENT。因此,文本不是完全可见的。

我是这样解决的:

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Build;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;


public class VerticalLabelView extends View
{
private final String LOG_TAG           = "VerticalLabelView";
private final int    DEFAULT_TEXT_SIZE = 30;
private int          _ascent           = 0;
private int          _leftPadding      = 0;
private int          _topPadding       = 0;
private int          _rightPadding     = 0;
private int          _bottomPadding    = 0;
private int          _textSize         = 0;
private int          _measuredWidth;
private int          _measuredHeight;
private Rect         _textBounds;
private TextPaint    _textPaint;
private String       _text             = "";
private TextView     _tempView;
private Typeface     _typeface         = null;
private boolean      _topToDown = false;


public VerticalLabelView(Context context)
{
super(context);
initLabelView();
}


public VerticalLabelView(Context context, AttributeSet attrs)
{
super(context, attrs);
initLabelView();
}


public VerticalLabelView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
initLabelView();
}


@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public VerticalLabelView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
{
super(context, attrs, defStyleAttr, defStyleRes);
initLabelView();
}


private final void initLabelView()
{
this._textBounds = new Rect();
this._textPaint = new TextPaint();
this._textPaint.setAntiAlias(true);
this._textPaint.setTextAlign(Paint.Align.CENTER);
this._textPaint.setTextSize(DEFAULT_TEXT_SIZE);
this._textSize = DEFAULT_TEXT_SIZE;
}


public void setText(String text)
{
this._text = text;
requestLayout();
invalidate();
}


public void topToDown(boolean topToDown)
{
this._topToDown = topToDown;
}


public void setPadding(int padding)
{
setPadding(padding, padding, padding, padding);
}


public void setPadding(int left, int top, int right, int bottom)
{
this._leftPadding = left;
this._topPadding = top;
this._rightPadding = right;
this._bottomPadding = bottom;
requestLayout();
invalidate();
}


public void setTextSize(int size)
{
this._textSize = size;
this._textPaint.setTextSize(size);
requestLayout();
invalidate();
}


public void setTextColor(int color)
{
this._textPaint.setColor(color);
invalidate();
}


public void setTypeFace(Typeface typeface)
{
this._typeface = typeface;
this._textPaint.setTypeface(typeface);
requestLayout();
invalidate();
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
try
{
this._textPaint.getTextBounds(this._text, 0, this._text.length(), this._textBounds);


this._tempView = new TextView(getContext());
this._tempView.setPadding(this._leftPadding, this._topPadding, this._rightPadding, this._bottomPadding);
this._tempView.setText(this._text);
this._tempView.setTextSize(TypedValue.COMPLEX_UNIT_PX, this._textSize);
this._tempView.setTypeface(this._typeface);


this._tempView.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);


this._measuredWidth = this._tempView.getMeasuredHeight();
this._measuredHeight = this._tempView.getMeasuredWidth();


this._ascent = this._textBounds.height() / 2 + this._measuredWidth / 2;


setMeasuredDimension(this._measuredWidth, this._measuredHeight);
}
catch (Exception e)
{
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
Log.e(LOG_TAG, Log.getStackTraceString(e));
}
}


@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);


if (!this._text.isEmpty())
{
float textHorizontallyCenteredOriginX = this._measuredHeight / 2f;
float textHorizontallyCenteredOriginY = this._ascent;


canvas.translate(textHorizontallyCenteredOriginY, textHorizontallyCenteredOriginX);


float rotateDegree = -90;
float y = 0;


if (this._topToDown)
{
rotateDegree = 90;
y = this._measuredWidth / 2;
}


canvas.rotate(rotateDegree);
canvas.drawText(this._text, 0, y, this._textPaint);
}
}
}

如果您希望从上到下都有一个文本,那么可以使用 topToDown(true)方法。

You can just add to your TextView or other View xml rotation value. This is the easiest way and for me working correct.

<LinearLayout
android:rotation="-90"
android:layout_below="@id/image_view_qr_code"
android:layout_above="@+id/text_view_savva_club"
android:layout_marginTop="20dp"
android:gravity="bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">


<TextView
android:textColor="@color/colorPrimary"
android:layout_marginStart="40dp"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Дмитриевский Дмитрий Дмитриевич"
android:maxLines="2"
android:id="@+id/vertical_text_view_name"/>
<TextView
android:textColor="#B32B2A29"
android:layout_marginStart="40dp"
android:layout_marginTop="15dp"
android:textSize="16sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/vertical_text_view_phone"
android:text="+38 (000) 000-00-00"/>


</LinearLayout>

Result

我最初在一个垂直 LinearLayout 中渲染垂直文本的方法如下(这是 Kotlin,在 Java 中使用 setRoatation等) :

val tv = TextView(context)
tv.gravity = Gravity.CENTER
tv.rotation = 90F
tv.height = calcHeight(...)
linearLabels.addView(tv)

approach #1

正如你所看到的,问题在于 TextView 是垂直的,但是仍然把它的宽度看作是水平的!=/

因此,方法 # 2包括额外的手动切换宽度和高度,以解释这一点:

tv.measure(0, 0)
// tv.setSingleLine()
tv.width = tv.measuredHeight
tv.height = calcHeight(...)

approach #2

然而,这会导致标签在相对较短的宽度之后绕到下一行(或者如果你裁剪 setSingleLine)。同样,这可以归结为混淆了 x 和 y。

因此,我的方法 # 3是将 TextView 封装在 RelativeLayout 中。这个想法是通过将 TextView 向左和向右扩展(在这里,两个方向都是200像素)来允许 TextView 任意宽度。但是,我给 RelativeLayout 设置了负边距,以确保它被绘制为一个狭窄的列。下面是截图的完整代码:

val tv = TextView(context)
tv.text = getLabel(...)
tv.gravity = Gravity.CENTER
tv.rotation = 90F


tv.measure(0, 0)
tv.width = tv.measuredHeight + 400  // 400 IQ
tv.height = calcHeight(...)


val tvHolder = RelativeLayout(context)
val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT)
lp.setMargins(-200, 0, -200, 0)
tvHolder.layoutParams = lp
tvHolder.addView(tv)
linearLabels.addView(tvHolder)


val iv = ImageView(context)
iv.setImageResource(R.drawable.divider)
linearLabels.addView(iv)

approach #3

作为一个总体的提示,这种拥有一个视图“持有”另一个视图的策略对我在 Android 中的定位非常有用!例如,ActionBar 下面的信息窗口使用相同的策略!

For text appearing like a store sign just insert newlines after each character, e.g. "N\nu\nt\ns" will be:

store sign example