Rótulo vertical (girado) no Android

110

Preciso de duas maneiras de mostrar o rótulo vertical no Android:

  1. Etiqueta horizontal virada 90 graus no sentido anti-horário (letras ao lado)
  2. Etiqueta horizontal com letras uma embaixo da outra (como uma placa de loja)

Preciso desenvolver widgets personalizados para os dois casos (um caso), posso fazer o TextView para renderizar dessa forma e qual seria uma boa maneira de fazer algo assim se eu precisar ir totalmente personalizado?

Bostone
fonte
É possível fazer isso em XML a partir da API 11 (Android 3.0). stackoverflow.com/questions/3774770/…
chobok de

Respostas:

239

Aqui está minha implementação de texto vertical simples e elegante, estendendo TextView. Isso significa que todos os estilos padrão de TextView podem ser usados, porque é TextView estendido.

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

Por padrão, o texto girado é de cima para baixo. Se você definir android: gravity = "bottom", ele será desenhado de baixo para cima.

Tecnicamente, ele engana o TextView subjacente ao pensar que é uma rotação normal (trocando largura / altura em alguns lugares), enquanto o desenho é girado. Funciona bem também quando usado em um layout xml.

EDIT: postar outra versão, acima tem problemas com animações. Esta nova versão funciona melhor, mas perde alguns recursos do TextView, como marquise e especialidades semelhantes.

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

EDITAR versão 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)
        }
    }
}
Pointer Null
fonte
2
Sua solução desativa links em TextView. Na verdade, os links estão sublinhados, mas não respondem ao clicar.
Gurnetko
5
isso tem problemas com várias linhas e barras de rolagem.
Cynichniy Bandera
1
desculpe pela minha pergunta básica, se eu tenho um TextView (em um arquivo XML) e quero girá-lo, como devo chamar a classe VerticalTextView?
blackst0ne de
2
@ blackst0ne em vez de tags <TextView>, use uma tag de visualização personalizada: <com.YOUR_PACKAGE_NAME.VerticalTextView>
Daiwik Daarun
1
funciona muito bem, no meu caso eu tive que estender android.support.v7.widget.AppCompatTextView em vez de TextView para fazer meu atributo de estilo funcionar
Louis
32

Implementei isso para meu projeto ChartDroid . Criar 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);
    }
}

E em 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>
Kostmo
fonte
Muito útil. Aqui está um link para a versão do tronco do seu projeto code.google.com/p/chartdroid/source/browse//trunk/core/…
rds
Não é útil, o texto não é mostrado totalmente .. é cortado do final e do início.
Siddarth G
Também trabalhou na versão 28 do Android acima
Ajeet Choudhary
Sua versão é a melhor opção para o meu projeto, mas setTextColor não está funcionando, também gostaria de aplicar um estilo (background e fontFamily), é possível fazer isso?
Pablo R.
9

Uma maneira de conseguir isso seria:

  1. Escreva sua própria visão personalizada e substitua onDraw (Canvas). Você pode desenhar o texto na tela e depois girar a tela.
  2. O mesmo que 1. exceto que desta vez use um Path e desenhe texto usando drawTextOnPath (...)
Prashast
fonte
Portanto, antes de seguir esse caminho (eu olhei para o método onDraw para TextView - é enorme), percebi que toda a diferença entre TextView e o botão de extensão é um ID de estilo interno (com.android.internal.R.attr.buttonStyle) Seria possível simplesmente definir um estilo personalizado e estender TextView semelhante a Button? Suponho que a resposta seria não, pois provavelmente não é possível estilizar o texto para o layout vertical
Bostone
Essa abordagem realmente funciona? Não tive sucesso e nem esse cara ... osdir.com/ml/Android-Developers/2009-11/msg02810.html
nategood
1
2. drawTextOnPath () desenha o texto como se fosse girado, igual a 1. Para escrever letras uma sob a outra, use \napós cada caractere ou se você tiver uma fonte de largura fixa, limite a largura de TextView para caber apenas um caractere.
blub de
8

Tentei ambas as classes VerticalTextView na resposta aprovada e elas funcionaram razoavelmente bem.

Mas não importa o que eu tentei, não consegui posicionar esses VerticalTextViews no centro do layout de conteúdo (um RelativeLayout que faz parte de um item inflado para um RecyclerView).

FWIW, depois de dar uma olhada, encontrei a classe VerticalTextView do yoog568 no GitHub:

https://github.com/yoog568/VerticalTextView/blob/master/src/com/yoog/widget/VerticalTextView.java

que consegui posicionar conforme desejado. Você também precisa incluir a seguinte definição de atributos em seu projeto:

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

albert c braun
fonte
1
Achei essa implementação muito fácil!
Hashim Akhtar
4
check = (TextView)findViewById(R.id.check);
check.setRotation(-90);

Isso funcionou para mim, muito bem. Quanto às letras que descem verticalmente - não sei.

ERJAN
fonte
7
mas ainda ocupa o espaço horizontalmente e o gira verticalmente
Prabs de
3

Existem algumas coisas menores que precisam ser prestadas atenção.

Depende do conjunto de caracteres ao escolher as formas de rotação ou de caminho. por exemplo, se o conjunto de caracteres de destino for semelhante ao inglês, e o efeito esperado for semelhante,

a
b
c
d

você pode obter este efeito desenhando cada personagem um por um, sem necessidade de rotação ou caminho.

insira a descrição da imagem aqui

você pode precisar de rotação ou caminho para obter este efeito.

a parte complicada é quando você tenta renderizar conjuntos de caracteres como o Mongolian. o glifo no Typeface precisa ser girado 90 graus, portanto, drawTextOnPath () será um bom candidato para uso.

Zephyr
fonte
Como isso pode ser feito de outra forma, do lado esquerdo para o lado direito
Rakesh
3
textview.setTextDirection (View.TEXT_DIRECTION_RTL) ou textview.setTextDirection (View.TEXT_DIRECTION_ANY_RTL) pode funcionar acima do nível de API 17. você pode testá-lo.
Zephyr
inteligente e simples
Abdulrahman Abdelkader
1

Seguindo a resposta de Pointer Null , fui capaz de centralizar o texto horizontalmente, modificando o onDrawmétodo desta forma:

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

Pode ser necessário adicionar uma parte de TextViewamedWidth para centralizar um texto multilinha.

dequec64
fonte
1

Você pode simplesmente adicionar ao seu TextView ou outro valor de rotação do View xml. Essa é a maneira mais fácil e para mim funcionando corretamente.

<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>

Resultado

Marina Budkovets
fonte
0

Minha abordagem inicial para renderizar texto vertical dentro de um LinearLayout vertical foi a seguinte (isso é Kotlin, em Java, setRoatationetc.):

val tv = TextView(context)
tv.gravity = Gravity.CENTER
tv.rotation = 90F
tv.height = calcHeight(...)
linearLabels.addView(tv)

abordagem # 1

Como você pode ver, o problema é que o TextView vai verticalmente, mas ainda trata sua largura como se fosse orientado horizontalmente! = /

Assim, a abordagem nº 2 consistia em alternar adicionalmente a largura e a altura manualmente para considerar isso:

tv.measure(0, 0)
// tv.setSingleLine()
tv.width = tv.measuredHeight
tv.height = calcHeight(...)

abordagem # 2

No entanto, isso resultou na quebra de etiquetas para a próxima linha (ou sendo cortadas se você setSingleLine) após a largura relativamente curta. Novamente, isso se resume a confundir x com y.

Minha abordagem nº 3 foi, portanto, envolver o TextView em um RelativeLayout. A ideia é permitir ao TextView qualquer largura que desejar estendendo-o para a esquerda e para a direita (aqui, 200 pixels em ambas as direções). Mas então eu dou ao RelativeLayout margens negativas para garantir que ele seja desenhado como uma coluna estreita. Aqui está meu código completo para esta captura de tela:

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)

abordagem # 3

Como dica geral, essa estratégia de ter uma visão "manter" outra visão tem sido muito útil para mim no posicionamento das coisas no Android! Por exemplo, a janela de informações abaixo da ActionBar usa a mesma tática!

Para o texto que aparece como um sinal de loja, basta inserir novas linhas após cada caractere, por exemplo "N\nu\nt\ns", será:

exemplo de placa de loja

xjcl
fonte
-1

Gostei da abordagem de @kostmo. Eu modifiquei um pouco, porque tive um problema - corte o rótulo girado verticalmente quando defini seus parâmetros como WRAP_CONTENT. Portanto, um texto não estava totalmente visível.

Foi assim que resolvi:

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

Se você quiser ter um texto de cima para baixo, use o topToDown(true)método.

Ayaz Alifov
fonte