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.
I implement a custom SpinNumberView: it is square shaped (say 40x40), it has a vertical LinearLayout as a subview, within this linear layout are a bunch of 40x40 cells stacked vertically. I want to animate the cells to scroll vertically by changing offsetY of the LinearLayout.
But there is one problem: only the cell initially in bounds (the first) is rendered, the cells outside of the bounds are not drawn, so when I animate the LinearLayout to scroll, the linear layout is spinning, but only the first cell is visible, others are blank spaces. Here is my entire code for the custom View:
public class SpinNumberView extends RelativeLayout {
private int startNumber;
private int endNumber;
private int number;
private int gridsize;
private int index;
public static final double stepDuration = 0.1;
private boolean inAnimation = true;
ArrayList<Integer> numbers;
public LinearLayout container;
public SpinNumberView(Context context) {
super(context);
}
public SpinNumberView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void dispatchDraw(Canvas canvas) {
// draw the background black solid circle
float radius = (float)(this.gridsize);
Paint p = new Paint();
p.setStyle(Paint.Style.FILL);
p.setARGB(192, 0, 0, 0);
canvas.drawCircle(radius/2, radius/2, radius/2, p);
// draw 1px white border
Paint pp = new Paint();
pp.setStyle(Paint.Style.STROKE);
pp.setStrokeWidth(2.0f);
pp.setARGB(192, 255, 255, 255);
canvas.drawCircle(radius/2, radius/2, radius/2-1, pp);
// clip to the circle
Path path = new Path();
RectF r = new RectF((float)0.0, (float)0.0, radius, radius);
path.addRoundRect(r, radius/2, radius/2, Path.Direction.CW);
canvas.clipPath(path);
super.dispatchDraw(canvas);
}
#Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
super.onLayout(b, i, i1, i2, i3);
}
class AniListener implements Animator.AnimatorListener {
#Override
public void onAnimationStart(Animator animator) {}
#Override
public void onAnimationEnd(Animator animator) {
SpinNumberView.this.animateStep();
}
#Override
public void onAnimationCancel(Animator animator) {}
#Override
public void onAnimationRepeat(Animator animator) {}
}
public void animateStep() {
this.container.setTranslationY(0);
float offset;
TimeInterpolator inter;
if(this.inAnimation) {
offset = (float)this.gridsize * this.numbers.size();
inter = new LinearInterpolator();
} else {
offset = (float)this.gridsize * this.index;
inter = new DecelerateInterpolator();
}
long duration = (long)(SpinNumberView.stepDuration * this.numbers.size() * 1000);
ViewPropertyAnimator ani = this.container.animate().translationYBy(-offset).setDuration(duration);
ani.setInterpolator(inter);
if(this.inAnimation) {
ani.setListener(new AniListener());
} else {
ani.setListener(null);
}
ani.start();
}
public void stopAnimation() {
this.inAnimation = false;
}
public void startAnimation() {
this.inAnimation = true;
float offset = (float)this.gridsize * this.numbers.size();
long duration = (long)(SpinNumberView.stepDuration * this.numbers.size() * 1000);
ViewPropertyAnimator ani = this.container.animate().translationYBy(-offset).setDuration(duration);
TimeInterpolator inter = new AccelerateInterpolator();
ani.setInterpolator(inter);
ani.setListener(new AniListener());
ani.start();
}
public void setup(int number, int start, int end, int gridsize) {
this.setBackgroundColor(Color.TRANSPARENT);
this.setAlpha((float) 0.5);
this.setClipChildren(false);
this.number = number;
this.startNumber = start;
this.endNumber = end;
this.gridsize = gridsize;
this.numbers = new ArrayList<>();
for(int i=start; i<=end;i++) {
this.numbers.add(i);
}
Collections.shuffle(this.numbers);
// Find index of target number within shuffled array
this.index = this.numbers.indexOf(this.number);
this.container = new LinearLayout(this.getContext());
this.container.setOrientation(LinearLayout.VERTICAL);
this.container.setGravity(Gravity.CENTER_HORIZONTAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(this.gridsize, this.gridsize * (this.numbers.size()+1));
this.container.setLayoutParams(params);
this.addView(this.container);
int offsety = 0;
// setup all the number views
for(int k=0;k<this.numbers.size()+1;k++) {
String txt;
if(k==this.numbers.size()) {
txt = Integer.toString(this.numbers.get(0));
} else {
txt = Integer.toString(this.numbers.get(k));
}
TextView tv = new TextView(this.getContext());
tv.setLayoutParams(new LayoutParams(this.gridsize, this.gridsize));
tv.setText(txt);
tv.setTextSize(24.0f);
tv.setTextColor(Color.WHITE);
tv.setTextAlignment(TextView.TEXT_ALIGNMENT_CENTER);
tv.setLines(1);
tv.setGravity(Gravity.CENTER_VERTICAL);
this.container.addView(tv);
offsety += this.gridsize;
}
this.invalidate();
}
}
Why is this happening?
BTW: I take a screenshot with getDrawingCache() of screen content, the cells are visible in the screenshot!
Yes! It happend when we get some view height or width of a view. Because didn't completely render the view when we call its height or width yet.
Solution:
Use this code to get Height and width
EditText edt = (EditText) findViewbyid(R.id.tv);
edt.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int height= edt.getHeight();
int width = edt.getHeight();
edt.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
To answer my own question:
When overriding onLayout() function, I need to layout the subviews myself like this:
#Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
super.onLayout(b, i, i1, i2, i3);
this.container.layout(0, 0, this.gridsize, this.gridsize * (this.endNumber-this.startNumber+2));
}
Glad you solved it by yourself, in iOS, we use something like Redraw method for these scenarios. Hopefully it will help you to further optimize your code.
I am working on expand and collapse of properties on CardView.
public class SimpleCardView extends CardView {
private int animationDuration;
public SimpleCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SimpleCardView(Context context) {
super(context);
}
public SimpleCardView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void expand(){
final int initialHeight = getHeight();
final float scale = getContext().getResources().getDisplayMetrics().density;
int targetHeight = (int) (232 * scale + 0.5f);
final int distance_to_expand = targetHeight - initialHeight;
Animation animation = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
getLayoutParams().height = (int) (initialHeight +(distance_to_expand*interpolatedTime));
requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
animationDuration = distance_to_expand;
animation.setDuration((long)distance_to_expand);
startAnimation(animation);
}
public int getAnimationTime(){
return animationDuration;
}
public void collapse(){}
}
This is my screenshot:
I am setting the constant value for target height.
int targetHeight = (int) (232 * scale + 0.5f);
Here, targetHeight is a expandable height of CardView.
Due to this, when the content is too long, only few portion of content is display.
Is there any way to set that height dynamically not a constant value?
Have you Tried with :wrap_content
android:layout_height="wrap_content"
<android.support.v7.widget.CardView
android:id="#+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
And you need to remove static height form class SimpleCardView.
**OR You need to calculate Height **
For this you need to set Text into TextView and then
calculate height of TextView With Below lines and then pass into SimpleCardView by making any setter method .
TextView textview ;
textveiw.setText("your text");
textview.mesure(0,0);
int height = textview.getMesuredHeight();
Hi Stackoverflow.
I've been trying to handle this issue for two days now.
We have a UnswipableViewPager, which is a custom implementation of ViewPager to intercept touch events and stop 'em (and nothing else), and right by it's right side we have a FrameLayout that we want to replace (through a FragmentTransaction) with our fragment. Nothing out of ordinary here if it wasn't for the fact our ViewPager has to shrink to fit the new Fragment. We have a custom implementation of RelativeLayout called ResizableLayout which we use to do that. It works ok with images, mind you, it's when we're loading a slide with a video, through a VideoView, that the issues pop.
This is how it looks from a design perspective. First we have it unshrunk, then we have it shrunk correctly, and last we have what happens whenever I try to load a slide with a VideoView inside it.
The snippet from the XML layout file:
<RelativeLayout
android:id="#+id/content_relative_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true">
<br.com.i9algo.taxiadv.v2.views.widgets.ResizableLayout
android:id="#+id/slideshow_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:clickable="true"
android:clipChildren="false"
android:orientation="horizontal"
android:scaleType="fitXY"
layout="#layout/slideshow_item_fragment">
<mypackage.widgets.UnswipableViewPager
android:id="#+id/playlist_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="#layout/slideshow_item_fragment"
android:clipChildren="false"
android:clickable="true"
android:scaleType="fitXY"
/>
</mypackage.widgets.ResizableLayout>
<FrameLayout
android:id="#+id/sidebar_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:clipChildren="false"
android:layout_toEndOf="#+id/slideshow_frame"
android:scaleType="fitXY" />
</RelativeLayout>
Our ResizableLayout class:
public class ResizableLayout extends RelativeLayout {
private int originalHeight = 0;
private int originalWidth = 0;
private int minWidth = 0;
private static final float SLIDE_TOP = 0f;
private static final float SLIDE_BOTTOM = 1f;
private boolean mMinimized = false;
public ResizableLayout(Context context) {
this(context, null, 0);
}
public ResizableLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ResizableLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
if (!isInEditMode()) {
minWidth = getContext().getResources().getDimensionPixelSize(R.dimen.playlist_min_width);
}
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public boolean smoothSlideTo(#NonNull float slideOffset) {
final int topBound = getPaddingTop();
int x = (int) (slideOffset * (getWidth() - getOriginalWidth()));
int y = (int) (topBound + slideOffset * getVerticalDragRange());
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
public void minimize() {
if (isMinimized())
return;
mMinimized = true;
try {
ResizeAnimation resizeAnimation = new ResizeAnimation(this, minWidth, getOriginalHeight(), false);
resizeAnimation.setDuration(500);
resizeAnimation.setTopMargin(20);
setAnimation(resizeAnimation);
smoothSlideTo(SLIDE_BOTTOM);
requestLayout();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void maximize() {
if (isMaximized())
return;
mMinimized = false;
try {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
params.width = getOriginalWidth();
params.topMargin = 0;
setLayoutParams(params);
measure(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
smoothSlideTo(SLIDE_TOP);
requestLayout();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public int getOriginalHeight() {
if (originalHeight == 0) {
originalHeight = getMeasuredHeight();
}
return originalHeight;
}
public int getOriginalWidth() {
if (originalWidth == 0) {
originalWidth = getMeasuredWidth();
}
return originalWidth;
}
public boolean isMinimized() {
return mMinimized;
}
public boolean isMaximized() {
return !mMinimized;
}
private float getVerticalDragRange() {
return getHeight() - getOriginalHeight();
}
This is ResizeAnimation in case anybody is wondering
public class ResizeAnimation extends Animation {
private final int mOriginalWidth;
private final int mOriginalHeight;
private final int mTargetWidth;
private final int mTargetHeight;
private int topMargin, leftMargin, bottomMargin, rightMargin;
private boolean mDown;
private View mView;
public ResizeAnimation(View view, int targetWidth, int targetHeight, boolean down) {
this.mView = view;
this.mTargetWidth = targetWidth;
this.mTargetHeight = targetHeight;
mOriginalWidth = view.getWidth();
mOriginalHeight = view.getHeight();
this.mDown = down;
}
public void setTopMargin(int value) {
this.topMargin = value;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newWidth = (int) (mOriginalWidth + (mTargetWidth - mOriginalWidth) * interpolatedTime);
int newHeight = (int) (mOriginalHeight + (mTargetHeight - mOriginalHeight) * interpolatedTime);
if (mDown) {
newWidth = mTargetWidth;
newHeight = mTargetHeight;
}
mView.getLayoutParams().width = newWidth;
mView.getLayoutParams().height = newHeight;
try {
((RelativeLayout.LayoutParams) mView.getLayoutParams()).topMargin = topMargin;
((RelativeLayout.LayoutParams) mView.getLayoutParams()).leftMargin = leftMargin;
((RelativeLayout.LayoutParams) mView.getLayoutParams()).bottomMargin = bottomMargin;
((RelativeLayout.LayoutParams) mView.getLayoutParams()).rightMargin = rightMargin;
} catch (Exception e) {
e.printStackTrace();
}
mView.requestLayout();
//mView.invalidate();
}
#Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
}
#Override
public boolean willChangeBounds() {
return true;
}
}
And this is the method that handles the FragmentTransaction.
#Override
public void showSidebarFragment() {
resizableLayout.minimize();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.sidebar_frame, sidebarFragment, "sidebarFragment");
ft.commit();
mContentRelativeLayout.requestLayout();
sidebarframe.requestLayout();
}
Mind you that Sidebarframe is injected through Butterknife and sidebarFragment is injected through Dagger2 - we use the same instance of the fragment for everything.
I have no clue what's going on. I've tried several ways of bringing the Fragment to front but nothing seems to work. I'd love if anyone could give me a hand either on how to fix the issue or how to achieve the same effect through other means - whatever works.
I need to make a credits screen (Activity) in my game. It would be just a vertically scrolling text lines without any images. Scrolling is to be performed automatically and no user interaction is allowed. Just like movie credits that goes from bottom to top. After the last text line has disappeared above top of the screen, it should restart.
How do I do it? It is sufficient to just use TextView and animate it somehow? Or should I put that TextView into ScrollView? What would you suggest?
I am using this :-
/**
* A TextView that scrolls it contents across the screen, in a similar fashion as movie credits roll
* across the theater screen.
*
* #author Matthias Kaeppler
*/
public class ScrollingTextView extends TextView implements Runnable {
private static final float DEFAULT_SPEED = 15.0f;
private Scroller scroller;
private float speed = DEFAULT_SPEED;
private boolean continuousScrolling = true;
public ScrollingTextView(Context context) {
super(context);
setup(context);
}
public ScrollingTextView(Context context, AttributeSet attributes) {
super(context, attributes);
setup(context);
}
private void setup(Context context) {
scroller = new Scroller(context, new LinearInterpolator());
setScroller(scroller);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (scroller.isFinished()) {
scroll();
}
}
private void scroll() {
int viewHeight = getHeight();
int visibleHeight = viewHeight - getPaddingBottom() - getPaddingTop();
int lineHeight = getLineHeight();
int offset = -1 * visibleHeight;
int totallineHeight = getLineCount() * lineHeight;
int distance = totallineHeight + visibleHeight;
int duration = (int) (distance * speed);
if (totallineHeight > visibleHeight) {
scroller.startScroll(0, offset, 0, distance, duration);
if (continuousScrolling) {
post(this);
}
}
}
#Override
public void run() {
if (scroller.isFinished()) {
scroll();
} else {
post(this);
}
}
public void setSpeed(float speed) {
this.speed = speed;
}
public float getSpeed() {
return speed;
}
public void setContinuousScrolling(boolean continuousScrolling) {
this.continuousScrolling = continuousScrolling;
}
public boolean isContinuousScrolling() {
return continuousScrolling;
}
}