Etiqueta vertical (girada) en Android

110

Necesito 2 formas de mostrar la etiqueta vertical en Android:

  1. Etiqueta horizontal girada 90 grados en sentido antihorario (letras en el lateral)
  2. Etiqueta horizontal con letras una debajo de la otra (como el letrero de una tienda)

¿Necesito desarrollar widgets personalizados para ambos casos (un caso), puedo hacer que TextView se renderice de esa manera, y cuál sería una buena manera de hacer algo así si necesito ser completamente personalizado?

Bostone
fuente
Es posible hacer esto en XML a partir de API 11 (Android 3.0). stackoverflow.com/questions/3774770/…
Chobok

Respuestas:

239

Aquí está mi implementación de texto vertical elegante y simple, extendiendo TextView. Esto significa que se pueden usar todos los estilos estándar de TextView, porque es TextView extendido.

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

De forma predeterminada, el texto girado es de arriba a abajo. Si configura android: gravity = "bottom", se dibuja de abajo hacia arriba.

Técnicamente, engaña al TextView subyacente al pensar que es una rotación normal (intercambiando ancho / alto en algunos lugares), mientras se dibuja girado. Funciona bien también cuando se usa en un diseño xml.

EDITAR: publicar otra versión, arriba tiene problemas con las animaciones. Esta nueva versión funciona mejor, pero pierde algunas características de TextView, como marquesina y especialidades similares.

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 la versión de 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)
        }
    }
}
Puntero nulo
fuente
2
Su solución deshabilita los enlaces en TextView. En realidad, los enlaces están subrayados, pero no responden al hacer clic.
Gurnetko
5
esto tiene problemas con las multilíneas y las barras de desplazamiento.
Cynichniy Bandera
1
perdón por mi pregunta básica, si tengo un TextView (en un archivo XML) y quiero rotarlo, ¿cómo tengo que llamar a la clase VerticalTextView?
blackst0ne
2
@ blackst0ne en lugar de las etiquetas <TextView>, use una etiqueta de vista personalizada: <com.YOUR_PACKAGE_NAME.VerticalTextView>
Daiwik Daarun
1
funciona muy bien, en mi caso tuve que extender android.support.v7.widget.AppCompatTextView en lugar de TextView para que mi atributo de estilo funcione
Louis
32

Implementé esto para mi proyecto ChartDroid . Crear 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);
    }
}

Y en 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
fuente
Muy útil. Aquí hay un enlace a la versión troncal de su proyecto code.google.com/p/chartdroid/source/browse//trunk/core/…
rds
No es útil, el texto no se muestra completamente ... está cortado desde el final y el comienzo.
Siddarth G
También trabajó en la versión anterior de Android 28
Ajeet Choudhary
Tu versión es la mejor opción para mi proyecto pero setTextColor no funciona, también me gustaría aplicar un estilo (background y fontFamily) ¿es posible hacerlo?
Pablo R.
9

Una forma de lograrlos sería:

  1. Escriba su propia vista personalizada y anule onDraw (Canvas). Puede dibujar el texto en el lienzo y luego rotar el lienzo.
  2. Igual que 1. excepto que esta vez use una Ruta y dibuje texto usando drawTextOnPath (...)
Prashast
fuente
Entonces, antes de seguir esa ruta (miré el método onDraw para TextView, es masivo) noté que toda la diferencia entre TextView y el botón extendido es una ID de estilo interno (com.android.internal.R.attr.buttonStyle) ¿Sería posible simplemente definir un estilo personalizado y extender TextView similar a Button? Supongo que la respuesta sería no, ya que probablemente no sea posible diseñar el texto para
diseñarlo
¿Funciona realmente este enfoque? No he tenido éxito y tampoco este tipo ... osdir.com/ml/Android-Developers/2009-11/msg02810.html
nategood
1
2. drawTextOnPath () dibuja el texto como si estuviera rotado, igual que 1. Para escribir letras una debajo de la otra, use \ndespués de cada carácter o si tiene una fuente de ancho fijo, limite el ancho de TextView para que quepa solo un carácter.
blub
8

Probé ambas clases de VerticalTextView en la respuesta aprobada y funcionaron razonablemente bien.

Pero no importa lo que intenté, no pude colocar esos VerticalTextViews en el centro del diseño contenedor (un RelativeLayout que es parte de un elemento inflado para un RecyclerView).

FWIW, después de mirar alrededor, encontré la clase VerticalTextView de yoog568 en GitHub:

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

que pude colocar como deseaba. También debe incluir la siguiente definición de atributos en su proyecto:

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

albert c braun
fuente
1
¡Encontré que esta implementación es realmente fácil!
Hashim Akhtar
4
check = (TextView)findViewById(R.id.check);
check.setRotation(-90);

Esto funcionó para mí, muy bien. En cuanto a las letras que descienden verticalmente, no lo sé.

ERJAN
fuente
7
pero incluso toma el espacio horizontalmente y lo gira verticalmente
Prabs
3

Hay algunas cosas menores a las que se debe prestar atención.

Depende del juego de caracteres al elegir las formas de rotación o ruta. por ejemplo, si el juego de caracteres de destino es como el inglés y el efecto esperado parece,

a
b
c
d

Puede obtener este efecto dibujando cada personaje uno por uno, sin necesidad de rotar o de ruta.

ingrese la descripción de la imagen aquí

es posible que necesite rotar o trazar para obtener este efecto.

la parte complicada es cuando intentas renderizar un juego de caracteres como mongol. el glifo en el tipo de letra debe rotarse 90 grados, por lo que drawTextOnPath () será un buen candidato para usar.

Céfiro
fuente
¿Cómo se puede hacer de otra manera de izquierda a derecha?
Rakesh
3
textview.setTextDirection (View.TEXT_DIRECTION_RTL) o textview.setTextDirection (View.TEXT_DIRECTION_ANY_RTL) pueden funcionar por encima del nivel de API 17. Puede probarlo.
Zephyr
inteligente y simple
Abdulrahman Abdelkader
1

Siguiendo la respuesta de Pointer Null , he podido centrar el texto horizontalmente modificando el onDrawmétodo de esta manera:

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

Es posible que deba agregar una parte del ancho de medida de TextView para centrar un texto de varias líneas.

dequec64
fuente
1

Simplemente puede agregar a su TextView u otro valor de rotación de View xml. Esta es la forma más fácil y para mí funciona correctamente.

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

Mi enfoque inicial para representar texto vertical dentro de un LinearLayout vertical fue el siguiente (esto es Kotlin, en uso de Java, setRoatationetc.):

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

enfoque # 1

Como puede ver, el problema es que TextView va verticalmente, ¡pero aún trata su ancho como si estuviera orientado horizontalmente! = /

Por lo tanto, el enfoque n. ° 2 consistió en cambiar adicionalmente el ancho y la altura manualmente para tener en cuenta esto:

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

enfoque # 2

Sin embargo, esto dio como resultado que las etiquetas se ajustaran a la siguiente línea (o se recortaran setSingleLine) después del ancho relativamente corto. Nuevamente, esto se reduce a confundir x con y.

Mi enfoque # 3 fue, por lo tanto, envolver el TextView en un RelativeLayout. La idea es permitir que TextView tenga el ancho que desee extendiéndolo hacia la izquierda y hacia la derecha (aquí, 200 píxeles en ambas direcciones). Pero luego doy los márgenes negativos de RelativeLayout para asegurarme de que se dibuje como una columna estrecha. Aquí está mi código completo para esta captura de pantalla:

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)

enfoque # 3

Como consejo general, esta estrategia de hacer que una vista "mantenga" otra vista ha sido realmente útil para mí para posicionar cosas en Android. Por ejemplo, la ventana de información debajo de ActionBar usa la misma táctica.

Para el texto que aparece como un letrero de tienda, simplemente inserte nuevas líneas después de cada carácter, por ejemplo "N\nu\nt\ns", será:

ejemplo de letrero de tienda

xjcl
fuente
-1

Me gustó el enfoque de @kostmo. Lo modifiqué un poco, porque tuve un problema: cortar la etiqueta girada verticalmente cuando configuré sus parámetros como WRAP_CONTENT. Por tanto, un texto no era completamente visible.

Así es como lo resolví:

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

Si desea tener un texto de arriba a abajo, utilice el topToDown(true)método.

Ayaz Alifov
fuente