Vertical (rotated) label in Android Vertical (rotated) label in Android android android

Vertical (rotated) label in Android


Here is my elegant and simple vertical text implementation, extending TextView. This means that all standard styles of TextView may be used, because it is extended 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);   }}

By default, rotated text is from top to bottom. If you set android:gravity="bottom", then it's drawn from bottom to top.

Technically, it fools underlying TextView to think that it's normal rotation (swapping width/height in few places), while drawing it rotated.It works fine also when used in an xml layout.

EDIT: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();  }}

EDITKotlin version:

import android.content.Contextimport android.graphics.Canvasimport android.text.BoringLayoutimport android.text.Layoutimport android.text.TextUtils.TruncateAtimport android.util.AttributeSetimport android.view.Gravityimport androidx.appcompat.widget.AppCompatTextViewimport androidx.core.graphics.withSaveclass 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)        }    }}


I implemented this for my ChartDroid project. Create 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>


Tried both of the VerticalTextView classes in the approved answer, and they worked reasonably well.

But no matter what I tried, I was unable to position those VerticalTextViews in the center of the containing layout (a RelativeLayout which is part of an item inflated for a RecyclerView).

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

which I was able to position as desired. You also need to include the following attributes definition in your project:

https://github.com/yoog568/VerticalTextView/blob/master/res/values/attr.xml