Android中的垂直(旋转)标签
Posted
技术标签:
【中文标题】Android中的垂直(旋转)标签【英文标题】:Vertical (rotated) label in Android 【发布时间】:2010-11-18 12:00:10 【问题描述】:我需要 2 种在 android 中显示垂直标签的方法:
-
水平标签逆时针旋转 90 度(侧面有字母)
字母一个接一个的水平标签(如商店标志)
我是否需要为这两种情况(一种情况)开发自定义小部件,我可以让 TextView 以这种方式呈现吗?如果我需要完全自定义,那么做类似事情的好方法是什么?
【问题讨论】:
从 API 11 (Android 3.0) 开始可以在 XML 中执行此操作。 ***.com/questions/3774770/… 【参考方案1】:这是我优雅而简单的垂直文本实现,扩展了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:gravity="bottom",则从下往上绘制。
从技术上讲,它使底层 TextView 误以为它是正常旋转(在少数地方交换宽度/高度),同时绘制它旋转。 在 xml 布局中使用时也可以正常工作。
编辑: 发布另一个版本,上面有动画问题。这个新版本效果更好,但是失去了一些 TextView 的特性,比如 marquee 和类似的特性。
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();
编辑 Kotlin 版本:
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)
【讨论】:
您的解决方案禁用了TextView
中的链接。实际上链接带有下划线,但点击时没有响应。
这与多行和滚动条有关。
@blackst0ne 代替 我为我的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);
在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>
【讨论】:
非常有用。这是您项目的主干版本的链接code.google.com/p/chartdroid/source/browse//trunk/core/… 没用,文字没有完全显示出来..它从结尾和开头截断。 在 android 版本 28 以上也工作过 您的版本是我项目的最佳选择,但 setTextColor 不起作用,我也想应用一种样式(背景和 fontFamily)可以这样做吗?【参考方案3】:在批准的答案中尝试了两个 VerticalTextView 类,并且它们工作得相当好。
但无论我尝试什么,我都无法将那些 VerticalTextViews 定位在包含布局的中心(一个 RelativeLayout,它是为 RecyclerView 膨胀的项目的一部分)。
FWIW,看了一圈,在GitHub上找到了yoog568的VerticalTextView类:
https://github.com/yoog568/VerticalTextView/blob/master/src/com/yoog/widget/VerticalTextView.java
我可以根据需要定位。您还需要在项目中包含以下属性定义:
https://github.com/yoog568/VerticalTextView/blob/master/res/values/attr.xml
【讨论】:
我发现这个实现非常简单! 它忽略了复合drawables【参考方案4】:实现这些的一种方法是:
-
编写自己的自定义view and override onDraw(Canvas).您可以在画布上绘制文本,然后旋转画布。
与 1 相同。除了这次使用 Path 并使用 drawTextOnPath(...) 绘制文本
【讨论】:
所以在我走这条路之前(我查看了 TextView 的 onDraw 方法 - 它非常庞大),我注意到 TextView 和扩展 Button 之间的全部区别是内部样式 ID(com.android.internal.R. attr.buttonStyle) 是否可以简单地定义自定义样式并扩展类似于 Button 的 TextView?我猜答案是否定的,因为可能无法将文本样式设置为垂直布局 这种方法真的有效吗?我没有成功,这家伙也没有……osdir.com/ml/Android-Developers/2009-11/msg02810.html 2. drawTextOnPath() 像旋转一样绘制文本,与 1 相同。要将字母一个接一个地写,请在每个字符后使用\n
,或者如果您有固定宽度的字体,请将 TextView 宽度限制为仅适合一个字符.【参考方案5】:
有一些小事情需要注意。
选择旋转或路径方式时取决于字符集。例如,如果目标字符集是类似英文的,并且预期的效果看起来像,
a
b
c
d
您可以通过逐个绘制每个字符来获得此效果,无需旋转或路径。
您可能需要旋转或路径才能获得此效果。
棘手的部分是当您尝试渲染像蒙古语这样的字符集时。字体中的字形需要旋转 90 度,因此 drawTextOnPath() 将是一个很好的选择。
【讨论】:
从leftSide到RightSide如何做其他方式 textview.setTextDirection(View.TEXT_DIRECTION_RTL) 或 textview.setTextDirection(View.TEXT_DIRECTION_ANY_RTL) 可能在 API 级别 17 以上工作。您可以对其进行测试。 智能简单【参考方案6】:check = (TextView)findViewById(R.id.check);
check.setRotation(-90);
这对我有用,很好。至于垂直向下的字母 - 我不知道。
【讨论】:
但它甚至水平占用空间,并垂直旋转它【参考方案7】:按照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 测量宽度以使多行文本居中。
【讨论】:
【参考方案8】:您可以只添加到您的 TextView 或其他 View xml 旋转值。这是最简单的方法,对我来说工作正常。
<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_
android:layout_
android:orientation="vertical">
<TextView
android:textColor="@color/colorPrimary"
android:layout_marginStart="40dp"
android:textSize="20sp"
android:layout_
android:layout_
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_
android:layout_
android:id="@+id/vertical_text_view_phone"
android:text="+38 (000) 000-00-00"/>
</LinearLayout>
【讨论】:
【参考方案9】:我最初在垂直 LinearLayout 中呈现垂直文本的方法如下(这是 Kotlin,在 Java 中使用 setRoatation
等):
val tv = TextView(context)
tv.gravity = Gravity.CENTER
tv.rotation = 90F
tv.height = calcHeight(...)
linearLabels.addView(tv)
如您所见,问题在于 TextView 是垂直的,但仍将其宽度视为水平方向! =/
因此方法 #2 包括额外手动切换宽度和高度以解决此问题:
tv.measure(0, 0)
// tv.setSingleLine()
tv.width = tv.measuredHeight
tv.height = calcHeight(...)
然而,这导致标签在相对较短的宽度之后环绕到下一行(或者如果您 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)
作为一般提示,这种让视图“持有”另一个视图的策略对我在 Android 中定位事物非常有用!例如,ActionBar 下方的信息窗口使用相同的策略!
对于看起来像商店标志的文本,只需在每个字符后插入换行符,例如"N\nu\nt\ns"
将是:
【讨论】:
【参考方案10】:我喜欢@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)
方法。
【讨论】:
以上是关于Android中的垂直(旋转)标签的主要内容,如果未能解决你的问题,请参考以下文章