Etichetta verticale (ruotata) in Android


110

Ho bisogno di 2 modi per mostrare l'etichetta verticale in Android:

  1. Etichetta orizzontale ruotata di 90 gradi in senso antiorario (lettere sul lato)
  2. Etichetta orizzontale con lettere una sotto l'altra (come l'insegna di un negozio)

Devo sviluppare widget personalizzati per entrambi i casi (un caso), posso fare in modo che TextView esegua il rendering in quel modo e quale sarebbe un buon modo per fare qualcosa del genere se dovessi diventare completamente personalizzato?


È possibile farlo in XML a partire dall'API 11 (Android 3.0). stackoverflow.com/questions/3774770/...
chobok

Risposte:


239

Ecco la mia implementazione del testo verticale elegante e semplice, che estende TextView. Ciò significa che possono essere utilizzati tutti gli stili standard di TextView, poiché è TextView esteso.

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

Per impostazione predefinita, il testo ruotato va dall'alto verso il basso. Se imposti android: gravity = "bottom", viene disegnato dal basso verso l'alto.

Tecnicamente, inganna TextView sottostante a pensare che sia normale rotazione (scambiando larghezza / altezza in pochi punti), mentre il disegno è ruotato. Funziona bene anche se utilizzato in un layout xml.

EDIT: la pubblicazione di un'altra versione, sopra ha problemi con le animazioni. Questa nuova versione funziona meglio, ma perde alcune funzionalità di TextView, come la selezione e specialità simili.

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

MODIFICA versione 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)
        }
    }
}

2
La tua soluzione disabilita i collegamenti in TextView. In realtà i collegamenti sono sottolineati, ma non rispondono al clic.
Gurnetko

5
questo ha problemi con multilinea e barre di scorrimento.
Cynichniy Bandera

1
scusa per la mia domanda di base, se ho un TextView (in un file XML) e voglio ruotarlo, come devo chiamare la classe VerticalTextView?
blackst0ne

2
@ blackst0ne invece dei tag <TextView>, utilizza un tag di visualizzazione personalizzato: <com.YOUR_PACKAGE_NAME.VerticalTextView>
Daiwik Daarun

1
funziona alla grande, nel mio caso ho dovuto estendere android.support.v7.widget.AppCompatTextView invece di TextView per far funzionare il mio attributo di stile
Louis

32

L'ho implementato per il mio progetto ChartDroid . Crea 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 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>

Molto utile. Ecco un collegamento alla versione tronco del tuo progetto code.google.com/p/chartdroid/source/browse//trunk/core/…
rds

Non utile, il testo non viene mostrato completamente .. è tagliato dalla fine e dall'inizio.
Siddarth G

Ha funzionato anche nella versione 28 di Android sopra
Ajeet Choudhary il

La tua versione è l'opzione migliore per il mio progetto ma setTextColor non funziona, inoltre vorrei applicare uno stile (background e fontFamily) è possibile farlo?
Pablo R.

9

Un modo per ottenerli sarebbe:

  1. Scrivi la tua visualizzazione personalizzata e sovrascrivi onDraw (Canvas). Puoi disegnare il testo sulla tela e quindi ruotare la tela.
  2. Come 1. tranne che questa volta usa un percorso e disegna il testo usando drawTextOnPath (...)

Quindi, prima di intraprendere quella strada (ho esaminato il metodo onDraw per TextView - è enorme) ho notato che l'intera differenza tra TextView e il pulsante extending è un ID di stile interno (com.android.internal.R.attr.buttonStyle) Sarebbe possibile definire semplicemente uno stile personalizzato ed estendere TextView simile a Button? Immagino che la risposta sarebbe no poiché probabilmente non è possibile modellare il testo in modo verticale
Bostone

Questo approccio funziona davvero? Non ho avuto successo e nemmeno questo ragazzo ... osdir.com/ml/Android-Developers/2009-11/msg02810.html
nategood

1
2. drawTextOnPath () disegna il testo come se fosse stato ruotato, come 1. Per scrivere le lettere una sotto l'altra, usale \ndopo ogni carattere o se hai un font a larghezza fissa, limita la larghezza di TextView per adattarla a un solo carattere.
blub

8

Ho provato entrambe le classi VerticalTextView nella risposta approvata e hanno funzionato abbastanza bene.

Ma non importa quello che ho provato, non sono riuscito a posizionare quei VerticalTextViews al centro del layout contenitore (un RelativeLayout che fa parte di un elemento gonfiato per un RecyclerView).

FWIW, dopo essermi guardato intorno, ho trovato la classe VerticalTextView di yoog568 su GitHub:

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

che ho potuto posizionare come desiderato. È inoltre necessario includere la seguente definizione di attributi nel progetto:

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


1
Ho trovato questa implementazione davvero facile!
Hashim Akhtar

4
check = (TextView)findViewById(R.id.check);
check.setRotation(-90);

Questo ha funzionato per me, benissimo. Per quanto riguarda le lettere che scendono verticalmente, non lo so.


7
ma prende lo spazio anche orizzontalmente e lo ruota verticalmente
Prabs

3

Ci sono alcune cose minori su cui prestare attenzione.

Dipende dal set di caratteri quando si sceglie la rotazione o il percorso. ad esempio, se il set di caratteri target è simile all'inglese e l'effetto previsto è simile a,

a
b
c
d

puoi ottenere questo effetto disegnando ogni personaggio uno per uno, non è necessario ruotare o tracciare.

inserisci qui la descrizione dell'immagine

potrebbe essere necessario ruotare o tracciare per ottenere questo effetto.

la parte complicata è quando si tenta di rendere il set di caratteri come il mongolo. il glifo nel carattere tipografico deve essere ruotato di 90 gradi, quindi drawTextOnPath () sarà un buon candidato da usare.


Come si può fare in un altro modo da sinistra a destra
Rakesh

3
textview.setTextDirection (View.TEXT_DIRECTION_RTL) o textview.setTextDirection (View.TEXT_DIRECTION_ANY_RTL) potrebbe funzionare al di sopra del livello API 17. puoi testarlo.
Zephyr

intelligente e semplice
Abdulrahman Abdelkader

1

Seguendo la risposta di Pointer Null , sono stato in grado di centrare il testo orizzontalmente modificando il onDrawmetodo in questo modo:

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

Potrebbe essere necessario aggiungere una parte di TextView MeasuredWidth per centrare un testo multilinea.


1

Puoi semplicemente aggiungere al tuo TextView o un altro valore di rotazione xml della vista. Questo è il modo più semplice e per me funziona correttamente.

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

Risultato


0

Il mio approccio iniziale al rendering del testo verticale all'interno di un LinearLayout verticale era il seguente (questo è Kotlin, in Java, setRoatationecc.):

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

approccio # 1

Come puoi vedere il problema è che TextView va verticalmente ma tratta comunque la sua larghezza come se fosse orientata orizzontalmente! = /

Pertanto, l'approccio n. 2 consisteva nel cambiare manualmente larghezza e altezza in aggiunta per tenere conto di questo:

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

approccio # 2

Tuttavia, ciò ha comportato l'avvolgimento delle etichette sulla riga successiva (o il taglio se si setSingleLine) dopo la larghezza relativamente breve. Di nuovo, questo si riduce a confondere x con y.

Il mio approccio n. 3 era quindi avvolgere TextView in un RelativeLayout. L'idea è di consentire al TextView di qualsiasi larghezza desideri estendendolo molto a sinistra ea destra (qui, 200 pixel in entrambe le direzioni). Ma poi assegno margini negativi al RelativeLayout per assicurarmi che sia disegnato come una colonna stretta. Ecco il mio codice completo per questo screenshot:

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)

approccio n. 3

Come suggerimento generale, questa strategia di avere una vista "trattenere" un'altra vista è stata davvero utile per me nel posizionare le cose in Android! Ad esempio, la finestra informativa sotto la ActionBar utilizza la stessa tattica!

Per il testo che appare come un'insegna di un negozio basta inserire le nuove righe dopo ogni carattere, ad esempio "N\nu\nt\ns"sarà:

esempio di insegna del negozio


-1

Mi è piaciuto l'approccio di @ kostmo. L'ho modificato un po ', perché ho avuto un problema: tagliare l'etichetta ruotata verticalmente quando ho impostato i suoi parametri come WRAP_CONTENT. Pertanto, un testo non era completamente visibile.

Ecco come l'ho risolto:

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 vuoi avere un testo dall'alto verso il basso, usa il topToDown(true)metodo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.