Вертикальная (повернутая) метка в Android

Мне нужно 2 способа отображения вертикальной метки в Android:

  1. Горизонтальная метка повернута на 90 градусов против часовой стрелки (буквы сбоку)
  2. Горизонтальная метка с буквами одна под другой (как знак магазина)

Нужно ли разрабатывать собственные виджеты для обоих случаев (один случай), могу ли я сделать так, чтобы TextView отображался таким образом, и что было бы хорошим способом сделать что-то подобное, если бы мне нужно было полностью настраивать?

8 ответов

Решение

Вот моя элегантная и простая вертикальная реализация текста, расширяющая 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, такие как выделение и аналогичные особенности.

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

Я реализовал это для моего проекта 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>

Опробовал оба класса VerticalTextView в утвержденном ответе, и они работали достаточно хорошо.

Но что бы я ни пытался, мне не удалось расположить эти VerticalTextViews в центре содержащего макета (RelativeLayout, который является частью элемента, накачанного для RecyclerView).

FWIW, оглянувшись вокруг, я обнаружил класс yoog568 VerticalTextView на 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

Одним из способов достижения этого будет:

  1. Написать свой собственный вид и переопределить onDraw (Canvas). Вы можете нарисовать текст на холсте, а затем вращать холст.
  2. То же, что и 1. кроме этого времени используйте Path и рисуйте текст используя drawTextOnPath(...)
check = (TextView)findViewById(R.id.check);
check.setRotation(-90);

Это сработало для меня, просто отлично. Что касается вертикально идущих вниз букв - я не знаю.

Есть некоторые мелочи, на которые нужно обратить внимание.

Это зависит от кодировки при выборе поворота или пути пути. например, если целевой набор символов похож на английский, а ожидаемый эффект выглядит следующим образом,

a
b
c
d

Вы можете получить этот эффект, рисуя каждого персонажа один за другим, без поворота или пути.

вам может понадобиться повернуть или путь, чтобы получить этот эффект.

сложная часть, когда вы пытаетесь визуализировать кодировку, например, монгольскую. глиф в гарнитуре должен быть повернут на 90 градусов, поэтому drawTextOnPath() будет хорошим кандидатом для использования.

Вы можете просто добавить в свой 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>

После ответа 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 для центрирования многослойного текста.

Мой первоначальный подход к визуализации вертикального текста внутри вертикального 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 пикселей в обоих направлениях). Но затем я даю 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! Например, информационное окно под панелью действий использует ту же тактику!

Если текст выглядит как знак магазина, просто вставляйте новые строки после каждого символа, например "N\nu\nt\ns" будет:

Мне понравился подход @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) метод.

Другие вопросы по тегам