I'm trying to get a vertical seekbar going with the emulator, but I'm
sort of stuck. I can get the seekbar to display the way I want it to,
and I can get the progress to do what I want, and I can modify the
onTouchEvent to get the thumb to go vertically instead of
horizontally. What I can't do is get the thumb to move outside of the
default 29 horizontal pixels without using setThumbOffset(). This in
itself isn't a problem. The problem is coming from the fact that I
don't understand the thumbOffset at all -- I guess. I think I could (properly) resize the
widget, which I am pretty sure I'm not doing right. Or maybe I could
just use the thumbOffset if I could figure it out. Since I can
calculate the progress correctly I thought I would just use a linear
function of progress * (getTop() - getBottom()) of the widget but that
doesn't seem to do it. But I can't figure out what the offset is
centered around.
As a somewhat aside, I am really unsure if what I am doing in
onSizeChanged() is sane or if it's going to bite me in the ass later
on.
Here's the main.xml layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<com.mobilsemantic.mobipoll.SlideBar
android:id="#+id/slide"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:max="100"
android:progress="0"
android:secondaryProgress="25" />
<Button android:id="#+id/button"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="Hello, I am a Button" />
<TextView android:id="#+id/tracking"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
And the class (ignore the debugging junk):
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.SeekBar;
public class SlideBar extends SeekBar {
private int oHeight = 320, oWidth = 29;
private int oProgress = -1, oOffset = -1;;
private float xPos = -1, yPos = -1;
private int top = -1, bottom = -1, left = -1, right = -1;
public SlideBar(Context context) {
super(context);
}
public SlideBar(Context context, AttributeSet attrs)
{
super(context, attrs);
oOffset = this.getThumbOffset();
oProgress = this.getProgress();
}
public SlideBar(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
protected synchronized void onMeasure(int widthMeasureSpec, intheightMeasureSpec)
{
int height = View.MeasureSpec.getSize(heightMeasureSpec);
oHeight = height;
this.setMeasuredDimension(oWidth, oHeight);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(h, w, oldw, oldh);
}
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
left = l;
right = r;
top = t;
bottom = b;
}
protected void onDraw(Canvas c)
{
c.rotate(90);
c.translate(0,-29);
super.onDraw(c);
}
public boolean onTouchEvent(MotionEvent event)
{
xPos = event.getX();
yPos = event.getY();
float progress = (yPos-this.getTop())/(this.getBottom()-this.getTop());
oOffset = this.getThumbOffset();
oProgress = this.getProgress();
Log.d("offset" + System.nanoTime(), new Integer(oOffset).toString());
Log.d("progress" + System.nanoTime(), new Integer(oProgress).toString());
float offset;
offset = progress * (this.getBottom()-this.getTop());
this.setThumbOffset((int)offset);
Log.d("offset_postsetprogress" + System.nanoTime(), new Integer(oOffset).toString());
Log.d("progress_postsetprogress" + System.nanoTime(), new Integer(oProgress).toString());
this.setProgress((int)(100*event.getY()/this.getBottom()));
return true;
}
}
I've created a solution which works (at least for me, anyway) and creates a vertical SeekBar.
http://hackskrieg.wordpress.com/2012/04/20/working-vertical-seekbar-for-android/
This code will correctly select/deselect the thumb, move correctly, update the listener correctly (only when the progress changes!), update/draw the progress correctly, etc. I hope it helps you.
public class VerticalSeekBar extends SeekBar {
public VerticalSeekBar(Context context) {
super(context);
}
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public VerticalSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw);
}
#Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}
protected void onDraw(Canvas c) {
c.rotate(-90);
c.translate(-getHeight(), 0);
super.onDraw(c);
}
private OnSeekBarChangeListener onChangeListener;
#Override
public void setOnSeekBarChangeListener(OnSeekBarChangeListener onChangeListener){
this.onChangeListener = onChangeListener;
}
private int lastProgress = 0;
#Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onChangeListener.onStartTrackingTouch(this);
setPressed(true);
setSelected(true);
break;
case MotionEvent.ACTION_MOVE:
// Calling the super seems to help fix drawing problems
super.onTouchEvent(event);
int progress = getMax() - (int) (getMax() * event.getY() / getHeight());
// Ensure progress stays within boundaries of the seekbar
if(progress < 0) {progress = 0;}
if(progress > getMax()) {progress = getMax();}
// Draw progress
setProgress(progress);
// Only enact listener if the progress has actually changed
// Otherwise the listener gets called ~5 times per change
if(progress != lastProgress) {
lastProgress = progress;
onChangeListener.onProgressChanged(this, progress, true);
}
onSizeChanged(getWidth(), getHeight() , 0, 0);
onChangeListener.onProgressChanged(this, getMax() - (int) (getMax() * event.getY() / getHeight()), true);
setPressed(true);
setSelected(true);
break;
case MotionEvent.ACTION_UP:
onChangeListener.onStopTrackingTouch(this);
setPressed(false);
setSelected(false);
break;
case MotionEvent.ACTION_CANCEL:
super.onTouchEvent(event);
setPressed(false);
setSelected(false);
break;
}
return true;
}
public synchronized void setProgressAndThumb(int progress) {
setProgress(getMax() - (getMax()- progress));
onSizeChanged(getWidth(), getHeight() , 0, 0);
}
public synchronized void setMaximum(int maximum) {
setMax(maximum);
}
public synchronized int getMaximum() {
return getMax();
}
}
I just placed this vertical SeekBar inside a LinearLayout with layout_height set to FILL_PARENT and layout_width set to WRAP_CONTENT.
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:orientation="vertical" >
<com.safetyculture.jsadroidtablet.VerticalSeekBar
android:id="#+id/calculatorVerticalSeekBar"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="bottom"
android:max="4"
android:progress="2" />
</LinearLayout>
NOTE: You must set an OnSeekBarChangeListener, otherwise interacting with the SeekBar will produce NullPointerException.
you can download at http://560b.sakura.ne.jp/android/VerticalSlidebarExample.zip, i hope this may can help you
For API 11 and later, can use seekbar's XML attributes(android:rotation="270") for vertical effect.
<SeekBar
android:id="#+id/seekBar1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:rotation="270"/>
For older API level (ex API10),use: https://github.com/AndroSelva/Vertical-SeekBar-Android or see this sample here
You also have to update it's height & width as suggest by Iftikhar
In order
seekBar.setLayoutParams(
new ViewGroup.LayoutParams(convertDpToPixels(1.0f,mContext), ViewGroup.LayoutParams.WRAP_CONTENT));
//haven't tested..
where
public static int convertDpToPixels(float dp, Context context){
Resources resources = context.getResources();
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
resources.getDisplayMetrics()
);
}
Take a look at android source . I think you need to change at least trackTouchEvent and there maybe a few other places where you also need to swap the x,y coordinates to take into account your rotation of the control.
Could you leave the seekbar as horizontal, put it in a frame layout, then rotate the layout 90 degrees in the java? sounds doable...
Related
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.
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().
I am creating images in which when scrolled up, the image should get enlarged and remaining images should get smaller. Similarly when second image pushed up, it should enlarge and show. I used accordion type but nothing works. I searched but couldn't find.
In IPhone, it has features but I couldn't find how to create for android. This is for
same effect to be implemented in android.
I used ScaleAnimation with hide and show of layouts to open where image been placed inside the layout. But that also didn't work.
if(openLayout == panel1){
panel1.startAnimation(new ScaleAnimToHide(1.0f, 1.0f, 1.0f, 0.0f, 200, panel1, true));
}
Can someone help me to solve this issue?
Thanks in advance!!
I have created a basic custom view which replicates this behaviour, it's not completely the same but I think it's close enough for now, if it needs to be exactly the same this can be quickly achieved by modifying the updateChildViews() method. I wrote this class in 20 minutes so it's far from perfect, for a production ready solution some additional work has to be done. Generally this solution works with all kinds of child views, but to replicate the exact behaviour use an ImageView as background for your child views and set this property on the ImageViews:
android:scaleType="centerCrop"
Problems I see with my solution in it's current state:
Only works in vertical orientation
No view recycling.
Should be derived from AdapterView and not LinearLayout.
Anyway that's how it looks so far:
Here is the source code:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
public class Accordion extends LinearLayout {
private static final String LOG_TAG = Accordion.class.getSimpleName();
private double scrollProgress = 0.0;
private double topViewScaleFactor = 2.0;
private double collapsedViewHeight = 200.0;
private double expandedViewHeight = 700.0;
private double scrollProgressPerView = expandedViewHeight;
private final ScrollTouchListener touchListener = new ScrollTouchListener() {
#Override
protected void onScroll(float x, float y) {
scrollProgress += y;
if(scrollProgress < 0.0) {
scrollProgress = 0.0;
}
int viewCount = getChildCount();
double maxScrollProgress = (viewCount - 1) * scrollProgressPerView + 1;
if(scrollProgress > maxScrollProgress) {
scrollProgress = maxScrollProgress;
}
Log.i(LOG_TAG, String.format("Scroll Progress: %f", scrollProgress));
updateChildViews();
}
};
public Accordion(Context context) {
super(context);
this.setOnTouchListener(this.touchListener);
}
public Accordion(Context context, AttributeSet attrs) {
super(context, attrs);
this.setOnTouchListener(this.touchListener);
}
public Accordion(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.setOnTouchListener(this.touchListener);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateChildViews();
}
private void updateChildViews() {
int viewCount = getChildCount();
double progress = scrollProgress;
double overflow = 0;
for(int i = 0; i < viewCount; i++) {
View child = getChildAt(i);
if(child != null) {
if(progress >= scrollProgressPerView) {
progress -= scrollProgressPerView;
child.setVisibility(View.GONE);
setChildHeight(child, 0);
} else if (progress > 0) {
setChildHeight(child, expandedViewHeight - progress);
overflow = progress;
child.setVisibility(View.VISIBLE);
progress = 0;
} else {
if(overflow > 0) {
double height = collapsedViewHeight + overflow;
if(height > expandedViewHeight) {
height = expandedViewHeight;
}
setChildHeight(child, height);
overflow = 0;
} else {
setChildHeight(child, i > 0 ? collapsedViewHeight : expandedViewHeight);
}
child.setVisibility(View.VISIBLE);
}
}
}
}
private void setChildHeight(View child, double height) {
child.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, (int)height));
}
private static abstract class ScrollTouchListener implements OnTouchListener {
private static final String LOG_TAG = ScrollTouchListener.class.getSimpleName();
private boolean scrolling = false;
private float x = 0;
private float y = 0;
#Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = event.getX();
y = event.getY();
scrolling = true;
return true;
case MotionEvent.ACTION_UP:
scrolling = false;
return true;
case MotionEvent.ACTION_MOVE:
if (scrolling) {
float newX = event.getX();
float newY = event.getY();
float difX = x - newX;
float difY = y - newY;
onScroll(difX, difY);
x = newX;
y = newY;
}
return true;
default:
return false;
}
}
protected abstract void onScroll(float x, float y);
}
}
To use it just put it in a layout like this:
<at.test.app.Accordion xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="#drawable/alpen"/>
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="#drawable/alpen"/>
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="#drawable/alpen"/>
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="#drawable/alpen"/>
</at.test.app.Accordion>
If you have any additional questions feel free to ask!
I think if the container you are using is a ListView, then following must be useful.Just need to detect the center element of the list view, and apply the zooming effect to the image in that row.Following is the code that can be used to implement this strategy:
int visibleChildCount = (listView1.getLastVisiblePosition() - listView1.getFirstVisiblePosition()) + 1;
In your getView() method:
if(position==visibleChildCount/2)
{
//Center Element
//Apply the Transition Effect From the XML files to the Image to Grow.
}
I'm writing a custom View object, but I can't seem to get it to measure correctly. By looking at the View source code, I thought calling setMinimumHeight() and setMinimumWidth() would be enough (that's really all I need, a minimum size that the parent layout should respect). Here's my code:
public class MonthView extends View {
private final int minCellSize = 24;
public MonthView(Context context) {
super(context);
}
public MonthView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MonthView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final float scale = getContext().getResources().getDisplayMetrics().density;
setMinimumHeight((int) (minCellSize * scale * 6));
setMinimumWidth((int) (minCellSize * scale * 7));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.RED);
}
}
Pretty simple. I then embed it in a LinearLayout, something like this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<com.foghina.adtp.MonthView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="I am below the monthview!"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
However, the MonthView takes up the entire screen and the TextView is not visible. How can I correctly write my View so that it has a minimum height / width when wrap_content is used?
I figured it out eventually. I had to write my own version of View.getDefaultSize() that is just slightly different. Here's how I used it:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final float scale = getContext().getResources().getDisplayMetrics().density;
setMeasuredDimension(getSize((int) (minCellSize * scale * 7), widthMeasureSpec),
getSize((int) (minCellSize * scale * 6), heightMeasureSpec));
}
private static int getSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = size < specSize ? size : specSize;
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
Pretty nasty. I don't understand why the default View.getDefaultSize() doesn't work like that.
I think you must use setMeasureDimension()
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();
}
}