I need 2 ways of showing vertical label in Android:
Horizontal label turned 90 degrees counterclockwise (letters on the side)
Horizontal label with letters one under the other (like a store sign)
Do I need to develop custom widgets for both cases (one case), can I make TextView to render that way, and what would be a good way to do something like that if I need to go completely custom?
Here is my elegant and simple vertical text implementation, extending TextView. This means that all standard styles of TextView may be used, because it is extended 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);
}
}
By default, rotated text is from top to bottom. If you set android:gravity="bottom", then it's drawn from bottom to top.
Technically, it fools underlying TextView to think that it's normal rotation (swapping width/height in few places), while drawing it rotated.
It works fine also when used in an xml layout.
EDIT:
posting another version, above has problems with animations. This new version works better, but loses some TextView features, such as marquee and similar specialties.
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
Kotlin version:
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)
}
}
}
I implemented this for my ChartDroid project. Create 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);
}
}
And 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>
Tried both of the VerticalTextView classes in the approved answer, and they worked reasonably well.
But no matter what I tried, I was unable to position those VerticalTextViews in the center of the containing layout (a RelativeLayout which is part of an item inflated for a RecyclerView).
FWIW, after looking around, I found yoog568's VerticalTextView class on GitHub:
https://github.com/yoog568/VerticalTextView/blob/master/src/com/yoog/widget/VerticalTextView.java
which I was able to position as desired. You also need to include the following attributes definition in your project:
https://github.com/yoog568/VerticalTextView/blob/master/res/values/attr.xml
One way to achieve these would be:
Write your own custom view and override onDraw(Canvas). You can draw the text on the canvas and then rotate the canvas.
Same as 1. except this time use a Path and draw text using drawTextOnPath(...)
There are some minor things need to be pay attention on.
It depends on the charset when choosing the rotate or the path ways. for example, if the target charset is English like, and the expected effect looks like,
a
b
c
d
you can get this effect by drawing each character one by one, no rotate or path needed.
you may need rotate or path to get this effect.
the tricky part is when you try to render charset such like Mongolian. the glyph in the Typeface need to be rotated 90 degree, so drawTextOnPath() will be a good candidate to use.
check = (TextView)findViewById(R.id.check);
check.setRotation(-90);
This worked for me, just fine. As for the vertically going down letters - I dont' know.
Following Pointer Null's answer, I've been able to center the text horizontally by modifying the onDraw method this way:
#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();
}
You might need to add a portion of the TextView measuredWidth to center a multilined text.
You can just add to your TextView or other View xml rotation value. This is the easiest way and for me working correct.
<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>
My initial approach to rendering vertical text inside a vertical LinearLayout was as follows (this is Kotlin, in Java use setRoatation etc.):
val tv = TextView(context)
tv.gravity = Gravity.CENTER
tv.rotation = 90F
tv.height = calcHeight(...)
linearLabels.addView(tv)
As you can see the problem is that the TextView goes vertically but still treats its width as if it were oriented horizontally! =/
Thus approach #2 consisted of additionally switching width and height manually to account for this:
tv.measure(0, 0)
// tv.setSingleLine()
tv.width = tv.measuredHeight
tv.height = calcHeight(...)
This however resulted in the labels wrapping around to the next line (or being cropped if you setSingleLine) after the relatively short width. Again, this boils down to confusing x with y.
My approach #3 was thus to wrap the TextView in a RelativeLayout. The idea is to allow the TextView any width it wants by extending it far to the left and the right (here, 200 pixels in both directions). But then I give the RelativeLayout negative margins to ensure it is drawn as a narrow column. Here is my full code for this 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)
As a general tip, this strategy of having a view "hold" another view has been really useful for me in positioning things in Android! For example, the info window below the ActionBar uses the same tactic!
For text appearing like a store sign just insert newlines after each character, e.g. "N\nu\nt\ns" will be:
I liked #kostmo's approach. I modified it a bit, because I had an issue - cutting off vertically rotated label when I set its params as WRAP_CONTENT. Thus, a text was not fully visible.
This is how I solved it:
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);
}
}
}
If you want to have a text from top to down, then use topToDown(true) method.
Related
Due to the bug mentioned in I can not adjust the edittext linespacing and https://issuetracker.google.com/issues/37009353#comment17
I tried the following workaround, by having custom onTextChanged
LinedEditText.java:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
/**
* Created by yccheok on 24/3/2018.
*/
public class LinedEditText extends android.support.v7.widget.AppCompatEditText {
public LinedEditText(Context context) {
super(context);
initPaint();
}
public LinedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
public LinedEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x80000000);
}
#Override
protected void onDraw(Canvas canvas) {
int left = getLeft();
int right = getRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int height = getHeight();
int lineHeight = getLineHeight();
int count = (height-paddingTop-paddingBottom) / lineHeight;
float originalLineHeight = lineHeight / getLineSpacingMultiplier();
for (int i = 0; i < count; i++) {
float baseline = lineHeight * (i + 1) + paddingTop - mPaint.descent() - (lineHeight - originalLineHeight);
canvas.drawLine(
left + paddingLeft,
baseline,
right - paddingRight,
baseline,
mPaint
);
}
super.onDraw(canvas);
}
#Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
if (lengthBefore != lengthAfter) {
float add = getLineSpacingExtra();
float mul = getLineSpacingMultiplier();
setLineSpacing(0f, 1f);
setLineSpacing(add, mul);
}
}
private Paint mPaint = new Paint();
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<mypackage.LinedEditText
android:id="#+id/edit_text"
android:gravity="top"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:background="#android:color/transparent"
android:textSize="18sp"
android:singleLine="false"
android:lineSpacingMultiplier="2.5"
android:lineSpacingExtra="0dp"
android:inputType="textMultiLine" />
</FrameLayout>
However, it still have problem on the cursor. When you press ENTER to move the cursor to next line, the cursor is drawn by ignoring line spacing multiplier. When you start typing, only then the position of cursor will be corrected.
Please see screenshot
Before pressing ENTER
After pressing ENTER (Cursor is ignoring lineSpacingMultiplier)
Start typing (Cursor is considering lineSpacingMultiplier)
This is a pretty annoying bug, and Google is not yet fixing it.
I was wondering, does anyone of you come across a solid workaround? Do you mind to share with us?
Override onMeasure in edit and setBounds in cursor drawable.
Reference: https://github.com/hanks-zyh/LineHeightEditText/issues/1#issuecomment-503476003
You may try to override onKeyUp() and call setLineSpacing().
So, as most of you know, there is no text justifying inside a TextView in Android. So, I built a custom TextView to get around the problem. However, for some reason, sometimes punctuation marks break the line for some reason in some devices. I tested on an LG G3 and emulator (Nexus 4 running latest version) and a comma "," for instance breaks the justification on the LG G3 but not on the emulator.
If I add a Padding start and end (or left and right) of at least 2, the problem is solved. This looks very arbitrary to me.
Basically, my logic was that in order to justify the text, I would need to know the width of the TextView itself, construct the text into lines that are at maximum that length. Then, by finding the number of spaces in the line and the remaining empty space, stretch the " " (space) characters to be scaled according to remaining pixels (or, space in the view).
It works almost perfectly, and most of the time it supports RTL text as well.
here're some pictures of the text (a simple lorem impsum) with and without the offending marks (first one is on emulator nexus 4 running 7.1.1, second one is on LG G3 running v5.0)
Here's the code:
public class DTextView extends AppCompatTextView {
private boolean justify;
public DTextView(Context context) {
super(context);
}
public DTextView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public DTextView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void setJustify(boolean justify) {
this.justify = justify;
if (justify) {
justify();
}
}
private void init(#Nullable AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.DTextView, 0, 0);
justify = ta.getBoolean(R.styleable.DTextView_justify, false);
ta.recycle();
}
private SpannableStringBuilder justifyText() {
String[] words = getText().toString().split(" ");
setText("");
int maxLineWidth = getWidth() - getPaddingLeft() - getPaddingRight();
SpannableStringBuilder justifiedTextSpannable = new SpannableStringBuilder();
//This will build the new text with the lines rearranged so that they will have a width
//bigger than the View's own width
ArrayList<String> lines = new ArrayList<>(0);
String line = "";
for (String word : words) {
if (getWordWidth(line + word) < maxLineWidth) {
line += word + " ";
} else {
line = line.substring(0, line.length() - 1);
lines.add(line);
line = word + " ";
}
}
//Add the last line
lines.add(line);
for (int i = 0; i < lines.size() - 1; i++) {
justifiedTextSpannable.append(justifyLine(lines.get(i), maxLineWidth));
justifiedTextSpannable.append("\n");
}
justifiedTextSpannable.append(lines.get(lines.size() - 1));
return justifiedTextSpannable;
}
private SpannableString justifyLine(String line, float maxWidth) {
SpannableString sLine = new SpannableString(line);
float spaces = line.split(" ").length - 1;
float spaceCharSize = getWordWidth(" ");
float emptySpace = maxWidth - getWordWidth(line);
float newSpaceSize = (emptySpace / spaces) + spaceCharSize;
float scaleX = newSpaceSize / spaceCharSize;
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) == ' ') {
sLine.setSpan(new ScaleXSpan(scaleX), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return sLine;
}
private void justify() {
justify = false;
setText(justifyText());
invalidate();
}
private float getWordWidth(String word) {
return getPaint().measureText(word);
}
#Override
protected void onDraw(Canvas canvas) {
if (!justify)
super.onDraw(canvas);
else
justify();
}
}
I would very much appreciate anyone that can shed some light on this.
So, after looking a bit more at this: https://github.com/ufo22940268/android-justifiedtextview and TextView in general, I discovered that my main problem was my approach.
Using the approach of scaling the width of the " " characters was sound in theory, but after doing so, the width of the line changes again, as it seems that the width of the line is NOT the sum of its parts.
I have changed my approach and took inspiration from the link above, and so in my new approach I draw each character by itself, instead of drawing the whole line. If the text needs to be justified (based on a custom "justify" boolean attribute) then it will draw the line and justify it, else it will just draw the line.
Edit: I have changed the code now so that it also supports RTL texts. I will upload the code somewhere in the next few days.
Here's the result:
Here's the code:
public class DTextView extends AppCompatTextView {
private boolean justify;
private float textAreaWidth;
private float spaceCharSize;
private float lineY;
public DTextView(Context context) {
super(context);
}
public DTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public DTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
/**
* #param attrs the attributes from the xml
* This function loads all the parameters from the xml
*/
private void init(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.DTextView, 0, 0);
justify = ta.getBoolean(R.styleable.DTextView_justify, false);
ta.recycle();
}
#Override
protected void onDraw(Canvas canvas) {
drawText(canvas);
}
private void drawText(Canvas canvas) {
TextPaint paint = getPaint();
paint.setColor(getCurrentTextColor());
paint.drawableState = getDrawableState();
textAreaWidth = getMeasuredWidth() - (getPaddingLeft() + getPaddingRight());
spaceCharSize = paint.measureText(" ");
String text = getText().toString();
lineY = getTextSize();
Layout textLayout = getLayout();
if (textLayout == null)
return;
Paint.FontMetrics fm = paint.getFontMetrics();
int textHeight = (int) Math.ceil(fm.descent - fm.ascent);
textHeight = (int) (textHeight * getLineSpacingMultiplier() + textLayout.getSpacingAdd());
for (int i = 0; i < textLayout.getLineCount(); i++) {
int lineStart = textLayout.getLineStart(i);
int lineEnd = textLayout.getLineEnd(i);
float lineWidth = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, paint);
String line = text.substring(lineStart, lineEnd);
if (line.charAt(line.length() - 1) == ' ') {
line = line.substring(0, line.length() - 1);
}
if (justify && i < textLayout.getLineCount() - 1) {
drawLineJustified(canvas, line, lineWidth);
} else {
canvas.drawText(line, 0, lineY, paint);
}
lineY += textHeight;
}
}
private void drawLineJustified(Canvas canvas, String line, float lineWidth) {
TextPaint paint = getPaint();
float emptySpace = textAreaWidth - lineWidth;
int spaces = line.split(" ").length - 1;
float newSpaceSize = (emptySpace / spaces) + spaceCharSize;
float charX = 0;
for (int i = 0; i < line.length(); i++) {
String character = String.valueOf(line.charAt(i));
float charWidth = StaticLayout.getDesiredWidth(character, paint);
if (!character.equals(" ")) {
canvas.drawText(character, charX, lineY, paint);
}
if (character.equals(" ") && i != line.length() - 1)
charX += newSpaceSize;
else
charX += charWidth;
}
}
}
and the XML:
<il.co.drapp.views.text.DTextView
android:layout_width="match_parent"
android:inputType="textMultiLine|textNoSuggestions"
app:justify="true"
android:id="#+id/justifyText"
android:text="#string/article_dummy_text"
android:layout_height="wrap_content" />
Thanks to Aditya Vyas-Lakhan for the links
LIBRARY: https://github.com/bluejamesbond/TextJustify-Android
SUPPORTS: Android 2.0 to 5.X
SCREENSHOT
Try this way to justify text, it works for me
public class MainActivity extends Activity {
private JustifiedTextView mJTv;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mJTv=(JustifiedTextView) findViewById(R.id.activity_main_jtv_text);
mJTv.setText(getResources().getString(R.string.test));
mJTv.setTextSize(TypedValue.COMPLEX_UNIT_SP,20);
mJTv.setLineSpacing(15);
mJTv.setBackgroundColor(Color.RED);
mJTv.setAlignment(Align.LEFT);
mJTv.setTypeFace(Typeface.createFromAsset(getAssets(), "fonts/naskh_bold.ttf"));
}
}
XML
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/activity_main_jsv"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ir.noghteh.JustifiedTextView
android:id="#+id/activity_main_jtv_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:padding="25dp"
xmlns:noghteh="http://noghteh.ir"
noghteh:text="#string/hello_world"
noghteh:textColor="#color/text"
noghteh:textSize="18sp"
>
</ir.noghteh.JustifiedTextView>
</ScrollView>
https://github.com/navabi/JustifiedTextView
https://github.com/ufo22940268/android-justifiedtextview
https://github.com/PareshMayani/Android-JustifyText
Just user like bellow
<TextView
android:id="#+id/tvMessageDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:justificationMode="inter_word"/>
I am trying to build a circle containing numerous dots that eventually will be clickable (as much as 108 dots to fill out the border of a circle).
What i have done so far is to create 108 imageviews like this:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/circle_1"
android:src="#drawable/dot_complete"
android:layout_marginLeft="383dp"
android:layout_marginTop="214dp"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/circle_2"
android:src="#drawable/dot_complete"
android:layout_marginLeft="382dp"
android:layout_marginTop="214dp"
/>
<!-- And so on all the way up to 108 -->
The result looks like this
However i suspect this is a very bad method, so my question is what would be the better way to do this, considering i need to have onclickListener on each dot in order to show its info.
Thank you
I had a similar class laying around, with a small modification it can display three different types of drawables as "dots". The only thing you would have to do is to write the touch management.
Drawing 108 dots (three different types):
public class DotsView extends View {
private static final int dots = 108;
private static final int dotRadius = 20;
private Bitmap testBitmap1;
private Bitmap testBitmap2;
private Bitmap testBitmap3;
private RectF dotRect;
private Paint paint;
private int[] dotsStates = new int[dots];
public DotsView(Context context) {
super(context);
setupView(context);
}
public DotsView(Context context, AttributeSet attrs) {
super(context, attrs);
setupView(context);
}
public DotsView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setupView(context);
}
private void setupView(Context context) {
setWillNotDraw(false);
paint = new Paint();
paint.setAntiAlias(true);
test();
}
private void test() {
//THIS METHOD IS JUST A TEST THAT CHANGES THE DRAWABLES USED FOR SOME DOTS
for (int i = 2; i < 20; ++i) {
dotsStates[i] = 1;
}
for (int i = 50; i < 55; ++i) {
dotsStates[i] = 2;
}
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
initBitmaps();
invalidate();
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
destroyBitmaps();
}
private void initBitmaps() {
testBitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test_1);
testBitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test_2);
testBitmap3 = BitmapFactory.decodeResource(getResources(), R.drawable.test_3);
dotRect = new RectF(0, 0, dotRadius, dotRadius);
}
private boolean isBitmapValid(Bitmap bitmap) {
return bitmap != null && !bitmap.isRecycled();
}
private void destroyBitmaps() {
if (isBitmapValid(testBitmap1)) {
testBitmap1.recycle();
testBitmap1 = null;
}
if (isBitmapValid(testBitmap2)) {
testBitmap2.recycle();
testBitmap2 = null;
}
if (isBitmapValid(testBitmap3)) {
testBitmap3.recycle();
testBitmap3 = null;
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isBitmapValid(testBitmap1) && isBitmapValid(testBitmap2) && isBitmapValid(testBitmap3)) {
// apply padding to canvas:
final int width = canvas.getWidth();
final int height = canvas.getHeight();
final int squareSide = Math.min(width, height);
canvas.translate(width / 2f, height / 2f); // moving to the center of the View
final float outerRadius = squareSide / 2f;
final float innerRadius = outerRadius - dotRadius;
final float angleFactor = 360f / dots;
for (int i = 0; i < dots; ++i) {
canvas.save(); // creating a "checkpoint"
canvas.rotate(angleFactor * i);
canvas.translate(innerRadius, 0); //moving to the edge of the big circle
canvas.drawBitmap(dotsStates[i] == 0 ?
testBitmap1 :
dotsStates[i] == 1 ?
testBitmap2 : testBitmap3,
null, dotRect, paint);
canvas.restore(); //restoring a "checkpoint"
}
}
}
}
Your approach is super heavyweight. I'd instead recommend making a custom View class, within which you do these things:
Override the onDraw method to draw your circles directly onto the view's Canvas
Implement an onTouchEvent listener, checking the coordinates of the touch against the positions/radii of the circles you created - thus finding the circle (if any) which was tapped
Trigger a custom event like onCircleTapped(View v, int circleId) so that the containing view/activity/fragment can handle the event properly.
You can try this librarytire view
import the barlibrary
create ChartTireView
I hope this will help you.
I'm looking for a simple way to forget that I'm using a WebView to have justified text in my TextView. Has someone made a custom view for this? I'm well aware that I can do something like this:
WebView view = new WebView(this);
view.loadData("my html with text justification","text/html","utf-8");
But it gets ugly when you want to set the size, the color or other common properties of the TextView. There must be a more convenient way of doing it.
It was getting on my nerves, I admit it. I like the TextViews to look like TextViews in the code, and even if I'm using a WebView as the means of achieving the text-align:justified formatting, I don't want to look at it that way.
I created a custom view (ugly, probably bad) that implements the methods that I commonly use from the TextView and modifies the content of the WebView in order to reflect those changes.
Wether it's useful for someone else or a potential hazard I really don't know, for me it works, I've used it in several projects and haven't run into any issues. The only minor inconvenience is that I assume it as a bigger toll memory-wise but nothing to worry about if it's just one or two (correct me if I'm wrong).
The result is the following:
And the code for setting it programmatically is as simple as this:
JustifiedTextView J = new JustifiedTextView();
J.setText("insert your text here");
Of course it'd be stupid to leave it like that so I also added the methods for changing the font-size and the font-color which are basically all I use TextViews for. Meaning I can do something like this:
JustifiedTextView J = new JustifiedTextView();
J.setText("insert your text here");
J.setTextColor(Color.RED);
J.setTextSize(30);
And obtain the following result (images are cropped):
But, this is not to show us how it looks, it's to share how you've done it!
I know, I know.
Here's the full code. It also addresses Problems when setting transparent background and loading UTF-8 strings into the view. See the comments in reloadData() for details.
public class JustifiedTextView extends WebView{
private String core = "<html><body style='text-align:justify;color:rgba(%s);font-size:%dpx;margin: 0px 0px 0px 0px;'>%s</body></html>";
private String textColor = "0,0,0,255";
private String text = "";
private int textSize = 12;
private int backgroundColor=Color.TRANSPARENT;
public JustifiedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
this.setWebChromeClient(new WebChromeClient(){});
}
public void setText(String s){
this.text = s;
reloadData();
}
#SuppressLint("NewApi")
private void reloadData(){
// loadData(...) has a bug showing utf-8 correctly. That's why we need to set it first.
this.getSettings().setDefaultTextEncodingName("utf-8");
this.loadData(String.format(core,textColor,textSize,text), "text/html","utf-8");
// set WebView's background color *after* data was loaded.
super.setBackgroundColor(backgroundColor);
// Hardware rendering breaks background color to work as expected.
// Need to use software renderer in that case.
if(android.os.Build.VERSION.SDK_INT >= 11)
this.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
}
public void setTextColor(int hex){
String h = Integer.toHexString(hex);
int a = Integer.parseInt(h.substring(0, 2),16);
int r = Integer.parseInt(h.substring(2, 4),16);
int g = Integer.parseInt(h.substring(4, 6),16);
int b = Integer.parseInt(h.substring(6, 8),16);
textColor = String.format("%d,%d,%d,%d", r, g, b, a);
reloadData();
}
public void setBackgroundColor(int hex){
backgroundColor = hex;
reloadData();
}
public void setTextSize(int textSize){
this.textSize = textSize;
reloadData();
}
}
In just three steps, you can justify your web view text.
1)
// Justify tag
String justifyTag = "<html><body style='text-align:justify;'>%s</body></html>";
2)
// Concatenate your string with the tag to Justify it
String dataString = String.format(Locale.US, justifyTag, "my html with text justification");
3)
// Load the data in the web view
webView.loadDataWithBaseURL("", dataString, "text/html", "UTF-8", "");
Without webview solution is : https://github.com/merterhk/JustifiedTextView
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.view.View;
public class JustifiedTextView extends View {
String text;
ArrayList<Line> linesCollection = new ArrayList<Line>();
TextPaint textPaint;
Typeface font;
int textColor;
float textSize = 42f, lineHeight = 57f, wordSpacing = 15f, lineSpacing = 15f;
float onBirim, w, h;
float leftPadding, rightPadding;
public JustifiedTextView(Context context, String text) {
super(context);
this.text = text;
init();
}
private void init() {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textColor = Color.BLACK;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (font != null) {
font = Typeface.createFromAsset(getContext().getAssets(), "font/Trykker-Regular.ttf");
textPaint.setTypeface(font);
}
textPaint.setColor(textColor);
int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
w = resolveSizeAndState(minw, widthMeasureSpec, 1);
h = MeasureSpec.getSize(widthMeasureSpec);
onBirim = 0.009259259f * w;
lineHeight = textSize + lineSpacing;
leftPadding = 3 * onBirim + getPaddingLeft();
rightPadding = 3 * onBirim + getPaddingRight();
textPaint.setTextSize(textSize);
wordSpacing = 15f;
Line lineBuffer = new Line();
this.linesCollection.clear();
String[] lines = text.split("\n");
for (String line : lines) {
String[] words = line.split(" ");
lineBuffer = new Line();
float lineWidth = leftPadding + rightPadding;
float totalWordWidth = 0;
for (String word : words) {
float ww = textPaint.measureText(word) + wordSpacing;
if (lineWidth + ww + (lineBuffer.getWords().size() * wordSpacing) > w) {// is
lineBuffer.addWord(word);
totalWordWidth += textPaint.measureText(word);
lineBuffer.setSpacing((w - totalWordWidth - leftPadding - rightPadding) / (lineBuffer.getWords().size() - 1));
this.linesCollection.add(lineBuffer);
lineBuffer = new Line();
totalWordWidth = 0;
lineWidth = leftPadding + rightPadding;
} else {
lineBuffer.setSpacing(wordSpacing);
lineBuffer.addWord(word);
totalWordWidth += textPaint.measureText(word);
lineWidth += ww;
}
}
this.linesCollection.add(lineBuffer);
}
setMeasuredDimension((int) w, (int) ((this.linesCollection.size() + 1) * lineHeight + (10 * onBirim)));
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(0f, 10f, getMeasuredWidth(), 10f, textPaint);
float x, y = lineHeight + onBirim;
for (Line line : linesCollection) {
x = leftPadding;
for (String s : line.getWords()) {
canvas.drawText(s, x, y, textPaint);
x += textPaint.measureText(s) + line.spacing;
}
y += lineHeight;
}
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Typeface getFont() {
return font;
}
public void setFont(Typeface font) {
this.font = font;
}
public float getLineHeight() {
return lineHeight;
}
public void setLineHeight(float lineHeight) {
this.lineHeight = lineHeight;
}
public float getLeftPadding() {
return leftPadding;
}
public void setLeftPadding(float leftPadding) {
this.leftPadding = leftPadding;
}
public float getRightPadding() {
return rightPadding;
}
public void setRightPadding(float rightPadding) {
this.rightPadding = rightPadding;
}
public void setWordSpacing(float wordSpacing) {
this.wordSpacing = wordSpacing;
}
public float getWordSpacing() {
return wordSpacing;
}
public float getLineSpacing() {
return lineSpacing;
}
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}
class Line {
ArrayList<String> words = new ArrayList<String>();
float spacing = 15f;
public Line() {
}
public Line(ArrayList<String> words, float spacing) {
this.words = words;
this.spacing = spacing;
}
public void setSpacing(float spacing) {
this.spacing = spacing;
}
public float getSpacing() {
return spacing;
}
public void addWord(String s) {
words.add(s);
}
public ArrayList<String> getWords() {
return words;
}
}
}
This is the same JustifiedTextView class given by Juan (and edited by me), but extended to work with custom xml attributes you can use in your layout xml files. Even Eclipse layout editor will show your custom attributes in the attribute table, which is cool. I put this into an additional answer, in case you want to keep things clean and don't need xml attributes.
public class JustifiedTextView extends WebView{
private String core = "<html><body style='text-align:justify;color:rgba(%s);font-size:%dpx;margin: 0px 0px 0px 0px;'>%s</body></html>";
private String text;
private int textColor;
private int backgroundColor;
private int textSize;
public JustifiedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public JustifiedTextView(Context context, AttributeSet attrs, int i) {
super(context, attrs, i);
init(attrs);
}
#SuppressLint("NewApi")
public JustifiedTextView(Context context, AttributeSet attrs, int i, boolean b) {
super(context, attrs, i, b);
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray a=getContext().obtainStyledAttributes(
attrs,
R.styleable.JustifiedTextView);
text = a.getString(R.styleable.JustifiedTextView_text);
if(text==null) text="";
textColor = a.getColor(R.styleable.JustifiedTextView_textColor, Color.BLACK);
backgroundColor = a.getColor(R.styleable.JustifiedTextView_backgroundColor, Color.TRANSPARENT);
textSize = a.getInt(R.styleable.JustifiedTextView_textSize, 12);
a.recycle();
this.setWebChromeClient(new WebChromeClient(){});
reloadData();
}
public void setText(String s){
if(s==null)
this.text="";
else
this.text = s;
reloadData();
}
#SuppressLint("NewApi")
private void reloadData(){
if(text!=null) {
String data = String.format(core,toRgba(textColor),textSize,text);
Log.d("test", data);
this.loadDataWithBaseURL(null, data, "text/html","utf-8", null);
}
// set WebView's background color *after* data was loaded.
super.setBackgroundColor(backgroundColor);
// Hardware rendering breaks background color to work as expected.
// Need to use software renderer in that case.
if(android.os.Build.VERSION.SDK_INT >= 11)
this.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
}
public void setTextColor(int hex){
textColor = hex;
reloadData();
}
public void setBackgroundColor(int hex){
backgroundColor = hex;
reloadData();
}
public void setTextSize(int textSize){
this.textSize = textSize;
reloadData();
}
private String toRgba(int hex) {
String h = Integer.toHexString(hex);
int a = Integer.parseInt(h.substring(0, 2),16);
int r = Integer.parseInt(h.substring(2, 4),16);
int g = Integer.parseInt(h.substring(4, 6),16);
int b = Integer.parseInt(h.substring(6, 8),16);
return String.format("%d,%d,%d,%d", r, g, b, a);
}
}
Either add this as justified_text_view_attr.xml into your res/values/ folder, or merge it into your existing attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="JustifiedTextView">
<attr name="text" format="string" localization="suggested"/>
<attr name="textColor" format="color|reference" />
<attr name="backgroundColor" format="color|reference" />
<attr name="textSize" format="integer" min="1" />
</declare-styleable>
</resources>
Feel free to edit if you find any bugs.
I believe this simplest form. And I worked perfectly
package domo.suichbt.util;
import android.content.Context;
import android.text.Html;
import android.util.AttributeSet;
import android.widget.TextView;
public class JustifiedTextView extends TextView
{
private final String CORE_TEMPLATE = "<html><body style='text-
align:justify;margin: 0px 0px 0px 0px;'>%s</body></html>";
public JustifiedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}
public JustifiedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}
public JustifiedTextView(Context context) {
super(context);
setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}
public JustifiedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}
}
Insert xml example
<domo.suichbt.util.JustifiedTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/sw_titulo"
android:singleLine="false">
</domo.suichbt.util.JustifiedTextView>
Take a look at this link. It uses a WebView to be bale to fully justify the text of a CheckBox in Android. It also can be used exactly the same in TextView, since each CheckBox is in fact a TextView and a Button.
http://www.collegemobile.com/2014/09/justify-text-android-checkbox/
Let's say you have a normal TextView, with "Stackoverflow" written in it, Is it possible to rotate the TextView by -90°, to have the S at the bottom and the W at the top of the screen?
Of course I could write my text as an image, rotate it and use it that way, but I am interested in the text right now.
Thanks.
You can set your textview as you would normally do
for example:
<TextView android:id="#+id/txtview"
android:layout_height="fill_parent"
android:layout_width="wrap_content" />
and write a function in your activity to
reverse the characters in your text
insert \n after every characters
and then set the text to the TextView.
If you dont want to insert the \n, you will have to set the size of android:layout_width and play with font size not to have 2 characters fitting on the same line and no truncation
Edit
If I have understood you correctly, you can get what you want by using animation.
For example
Under res/anim/myanim.xml:
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="-90"
android:pivotX="50%"
android:duration="0" />
You will have to play with this file to define where you want your text view to be placed.
In your activity:
TextView t = (TextView)findViewById(R.id.txtview);
String txt = "Stackoverflow";
t.setText(txt);
RotateAnimation ranim = (RotateAnimation)AnimationUtils.loadAnimation(this, R.anim.myanim);
ranim.setFillAfter(true); //For the textview to remain at the same place after the rotation
t.setAnimation(ranim);
Worked for me:
public class VerticalTextView extends TextView {
private int _width, _height;
private final Rect _bounds = new Rect();
public VerticalTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public VerticalTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VerticalTextView(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// vise versa
_height = getMeasuredWidth();
_width = getMeasuredHeight();
setMeasuredDimension(_width, _height);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(_width, _height);
canvas.rotate(-90);
TextPaint paint = getPaint();
paint.setColor(getTextColors().getDefaultColor());
String text = text();
paint.getTextBounds(text, 0, text.length(), _bounds);
canvas.drawText(text, getCompoundPaddingLeft(), (_bounds.height() - _width) / 2, paint);
canvas.restore();
}
private String text() {
return super.getText().toString();
}
}
xml:
<VerticalTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|center_vertical"
android:background="#color/feedback_background"
android:padding="4dip"
android:text="#string/feedback"
android:textColor="#color/feedback_text_color"
android:textSize="#dimen/text_xlarge" />
<TextView
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text="xyz"
android:rotation="-90"
android:gravity="fill_vertical"/>
Try this. It works fine for me. It can display one line of text vertically, but just one line. colors, size, paddings, margins and background all work fine.
public class VerticalTextView extends TextView {
public VerticalTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public VerticalTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VerticalTextView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
final ColorStateList csl = getTextColors();
final int color = csl.getDefaultColor();
final int paddingBottom = getPaddingBottom();
final int paddingTop = getPaddingTop();
final int viewWidth = getWidth();
final int viewHeight = getHeight();
final TextPaint paint = getPaint();
paint.setColor(color);
final float bottom = viewWidth * 9.0f / 11.0f;
Path p = new Path();
p.moveTo(bottom, viewHeight - paddingBottom - paddingTop);
p.lineTo(bottom, paddingTop);
canvas.drawTextOnPath(getText().toString(), p, 0, 0, paint);
}
}
If you are using API 11 or later, you may try:
TextView t = (TextView) findViewById(R.id.txtview);
String txt = "Stackoverflow";
t.setText(txt);
t.setRotation(90); // 90 degree rotation
I'll show for you guys my example of custom vertical button with the rotated TextView in it:
<!--Undo button-->
<LinearLayout
android:id="#+id/undo_points_pr_a"
android:layout_width="#dimen/zero_dp"
android:gravity="center"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1"
android:background="#color/timerUndoButton">
<ImageView
android:layout_width="#dimen/large"
android:layout_height="#dimen/large"
android:src="#drawable/undo_icon"
android:rotation="-90"
android:layout_marginBottom="#dimen/medium"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/undo"
android:textSize="#dimen/small_medium_text"
android:rotation="-90"/>
</LinearLayout>
And this is how it looks in Android Studio:
And of course you have to modify this code to make it works for you. (in attributes like android:layout_width, android:layout_height, etc.)
I provided a solution in another StackOverflow question. You can get vertical TextView by extending from View and overriding its onMeasure() and onDraw() methods. However, it will not support all TextView features, rather its main ones like padding, size, color and font.
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);
}
}
}
I think the simplest answer to your question to write "Stackoverflow" vertically is to use an ordinary TextView, and since the text will wrap to the next line when narrowed, play around with the width of the TextView so there is one letter is on each line and if you need more space on the edge as a buffer increase the "padding" and/or "margin" of the TextView.
My initial approach to rendering vertical text inside a vertical LinearLayout was as follows (this is Kotlin, in Java use setRoatation etc.):
val tv = TextView(context)
tv.gravity = Gravity.CENTER
tv.rotation = 90F
tv.height = calcHeight(...)
linearLabels.addView(tv)
As you can see the problem is that the TextView goes vertically but still treats its width as if it were oriented horizontally! =/
Thus approach #2 consisted of additionally switching width and height manually to account for this:
tv.measure(0, 0)
// tv.setSingleLine()
tv.width = tv.measuredHeight
tv.height = calcHeight(...)
This however resulted in the labels wrapping around to the next line (or being cropped if you setSingleLine) after the relatively short width. Again, this boils down to confusing x with y.
My approach #3 was thus to wrap the TextView in a RelativeLayout. The idea is to allow the TextView any width it wants by extending it far to the left and the right (here, 200 pixels in both directions). But then I give the RelativeLayout negative margins to ensure it is drawn as a narrow column. Here is my full code for this 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)
As a general tip, this strategy of having a view "hold" another view has been really useful for me in positioning things in Android! For example, the info window below the ActionBar uses the same tactic!
For text starting at the bottom just rotate it by -90F instead of 90F degrees.
public class VerticalTextView extends AppCompatTextView {
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();
}
}