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


110

Мені потрібні два способи відображення вертикальної мітки в Android:

  1. Горизонтальна мітка повернула на 90 градусів проти годинникової стрілки (літери збоку)
  2. Горизонтальна етикетка з літерами одна під іншою (як знак магазину)

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


Це можливо зробити в XML як API 11 (Android 3.0). stackoverflow.com/questions/3774770 / ...
chobok

Відповіді:


239

Ось моя елегантна і проста вертикальна реалізація тексту, що розширює 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 = "знизу", то він намальований знизу вгору.

Технічно він обдурює основу TextView, думаючи, що це нормальне обертання (міняючи ширину / висоту в декількох місцях), при цьому малюнок повертається. Він чудово працює і при використанні у форматі XML.

РЕДАКТУВАННЯ: розміщення іншої версії вище має проблеми з анімацією. Ця нова версія працює краще, але втрачає деякі функції TextView, такі як marquee та подібні спеціальності.

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

EDIT Версія Котліна:

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
Ваше рішення відключає посилання в TextView. Насправді посилання підкреслені, але не відповідають на натискання.
Гурнетко

5
тут виникають проблеми з багаторядковими і смугами прокрутки.
Синічний Бандера

1
вибачте за моє основне запитання, якщо я отримав TextView (у XML-файлі) і хочу повернути його, як я повинен викликати клас VerticalTextView?
blackst0ne

2
@ blackst0ne замість тегів <TextView>, використовуйте спеціальний тег подання: <com.YOUR_PACKAGE_NAME.VerticalTextView>
Daiwik Daarun

1
відмінно працює, в моєму випадку мені довелося розширити android.support.v7.widget.AppCompatTextView замість TextView, щоб мій атрибут стилю працював
Луї

32

Я реалізував це для свого проекту 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>

Дуже корисний. Ось посилання на магістральну версію вашого проекту code.google.com/p/chartdroid/source/browse//trunk/core/…
rds

Не корисно, текст не відображається повністю .. його відрізають від кінця і починають.
Siddarth G

Також працював над версією Android для Android 28
Ajeet Choudhary

Ваша версія є найкращим варіантом для мого проекту, але setTextColor не працює, також я хотів би застосувати стиль (фон і шрифтFamily) чи можливо це зробити?
Пабло Р.

9

Одним із способів цього досягти:

  1. Напишіть власний перегляд та перейдіть на Draw (Полотно). Ви можете намалювати текст на полотні, а потім повернути полотно.
  2. Як і 1. За винятком цього разу використовуйте Шлях та малюйте текст, використовуючи drawTextOnPath (...)

Отже, перш ніж пройти цей маршрут (я переглянув метод onDraw для TextView - він є масовим), я помітив, що вся різниця між TextView і кнопкою, що розширюється, - це внутрішній ідентифікатор стилю (com.android.internal.R.attr.buttonStyle) просто визначити власний стиль та розширити TextView, подібний до кнопки? Я здогадуюсь, що відповідь буде "ні", оскільки, мабуть, неможливо стилізувати текст до вертикальної верстки
Bostone,

Чи діє цей підхід насправді? Я не мав успіху і не мав цього хлопця ... osdir.com/ml/Android-Developers/2009-11/msg02810.html
nategood

1
2. drawTextOnPath () малює текст так, як він був повернутий, так само, як 1. Щоб писати літери один під іншим, будь-ласка, використовуйте \nпісля кожного символу або якщо у вас є шрифт фіксованої ширини, обмежте ширину TextView, щоб відповідати лише одному символу.
blub

8

Спробували обидва класи VerticalTextView у схваленій відповіді, і вони працювали досить добре.

Але незалежно від того, що я намагався, мені не вдалося розмістити ті VerticalTextViews в центрі вміщуючого макета (RelativeLayout, який є частиною елемента, завищеного для RecyclerView).

FWIW, оглянувши навколо, виявив клас G вертикальний текст на GitHub yoog568:

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
Я знайшов цю реалізацію дуже простою!
Хашим Ахтар

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

Це працювало для мене, просто чудово. Що стосується вертикально спускаються літер - я не знаю.


7
але він навіть займає простір горизонтально і обертає його вертикально
Прабс

3

На це потрібно звернути увагу на деякі незначні речі.

Це залежить від діаграми при виборі повороту або шляхів шляху. наприклад, якщо цільова діаграма є англійською мовою, і очікуваний ефект виглядає так,

a
b
c
d

Ви можете отримати цей ефект, намалювавши кожного символу один за одним, не потрібно обертати або прокладати шлях.

введіть тут опис зображення

Вам може знадобитися поворот або шлях, щоб отримати цей ефект.

складна частина полягає в тому, коли ви намагаєтеся надати таку схему, як монгольська. гліф у Typeface потрібно повернути на 90 градусів, тому drawTextOnPath () буде хорошим кандидатом для використання.


Як це можна зробити з іншого боку наліво наліво на Правостороння
Ракеш

3
textview.setTextDirection (View.TEXT_DIRECTION_RTL) або textview.setTextDirection (View.TEXT_DIRECTION_ANY_RTL) може працювати вище рівня API 17. Ви можете його протестувати.
Зефір

розумний і простий
Абдулрахман Абделькадер

1

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


1

Ви можете просто додати у TextView або інше значення перегляду 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>

Результат


0

Мій початковий підхід до візуалізації вертикального тексту всередині вертикалі LinearLayout був наступним (це Котлін, у використанні Java setRoatationтощо):

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

підхід №1

Як ви бачите, проблема полягає в тому, що TextView йде вертикально, але все ще розглядає свою ширину так, ніби орієнтований горизонтально! = /

Таким чином, підхід №2 складався з додаткового перемикання ширини та висоти вручну для врахування цього:

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

підхід №2

Однак це призвело до того, що мітки загортаються до наступного рядка (або обрізаються, якщо ви setSingleLine) після відносно невеликої ширини. Знову ж таки, це зводиться до плутання x з y.

Таким чином, мій підхід №3 полягав у тому, щоб перетворити TextView у відносний макет. Ідея полягає в тому, щоб дозволити 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)

підхід №3

Як загальна порада, ця стратегія перегляду «утримувати» інший погляд була дуже корисною для мене при позиціонуванні речей в Android! Наприклад, інформаційне вікно під ActionBar використовує ту саму тактику!

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

приклад знака магазину


-1

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.