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 代替 标签,使用自定义视图标签: 效果很好,在我的情况下,我必须扩展 android.support.v7.widget.AppCompatTextView 而不是 TextView 以使我的样式属性正常工作 在 android:rotation="270" 运行后,它也占用了水平视图的空间【参考方案2】:

我为我的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中的垂直(旋转)标签的主要内容,如果未能解决你的问题,请参考以下文章

在Android中的旋转木马

如何在android中旋转按钮的文本?

Android 垂直标签主机

带有水平溢出的Android垂直recyclerview

Android屏幕旋转

Android垂直对齐文本和小部件