【问题标题】:Vertical (rotated) label in AndroidAndroid中的垂直(旋转)标签
【发布时间】:2010-11-18 12:00:10
【问题描述】:

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

  1. 水平标签逆时针旋转 90 度(侧面有字母)
  2. 字母一个接一个的水平标签(如商店标志)

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

【问题讨论】:

标签: android label textview vertical-alignment


【解决方案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】:

实现这些的一种方法是:

  1. 编写自己的自定义view and override onDraw(Canvas).您可以在画布上绘制文本,然后旋转画布。
  2. 与 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_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>
    

    【讨论】:

      【解决方案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)方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-08-18
          • 2016-08-04
          • 2013-03-11
          • 2013-03-22
          • 2012-12-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多