I have added a seekbar to one of my activities.
Its max value is 5. Now, I want to display the divider values (with increment 1, like 0, 1, 2, 3, 4 and 5) below my seekbar. How can I do that?
Is there any system method to achieve this which I am not able to put my hands on? Any inputs are welcomed.
NOTE : I want to apply any changes programatically, not from xml. The numbers should be separated at equal intervals. I could not edit it that precisely though.
I am supposing you want to display view like below in picture.
if that is the case you have to create your own customSeekbar like give code.
CustomSeekBar.java
public class CustomSeekBar extends SeekBar {
private Paint textPaint;
private Rect textBounds = new Rect();
private String text = "";
public CustomSeekBar(Context context) {
super(context);
textPaint = new Paint();
textPaint.setColor(Color.WHITE);
}
public CustomSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
textPaint = new Paint();
textPaint.setTypeface(Typeface.SANS_SERIF);
textPaint.setColor(Color.BLACK);
}
public CustomSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
textPaint = new Paint();
textPaint.setColor(Color.BLACK);
}
#Override
protected synchronized void onDraw(Canvas canvas) {
// First draw the regular progress bar, then custom draw our text
super.onDraw(canvas);
int progress = getProgress();
text = progress + "";
// Now get size of seek bar.
float width = getWidth();
float height = getHeight();
// Set text size.
textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
textPaint.setTextSize(40);
// Get size of text.
textPaint.getTextBounds(text, 0, text.length(), textBounds);
// Calculate where to start printing text.
float position = (width / getMax()) * getProgress();
// Get start and end points of where text will be printed.
float textXStart = position - textBounds.centerX();
float textXEnd = position + textBounds.centerX();
// Check does not start drawing outside seek bar.
if (textXStart <= 1) textXStart = 20;
if (textXEnd > width) {
textXStart -= (textXEnd - width + 30);
}
// Calculate y text print position.
float yPosition = height;
canvas.drawText(text, textXStart, yPosition, textPaint);
}
public synchronized void setTextColor(int color) {
super.drawableStateChanged();
textPaint.setColor(color);
drawableStateChanged();
}
}
In your Xml file use your custom file like below
<com.waleedsarwar.customseekbar.CustomSeekBar
android:id="#+id/seekbar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:max="5"
android:paddingBottom="16dp" />
This is another approach. I am extending a linearlayout. I put seekbar and another linearlayout(layout_5) which contains 6 textviews with 0-1-2-3-4-5. Better option would be creating a dynamic image(get width from seekBar) which has these numbers according to segment count.
I force seekbar's indicator to stop at specific points(6 points in your case). Instead of doing this, it is possible to set seekBar's maximum progress value to 5. It will work, but it will not give a good user experience.
public class SegmentedSeekBar extends LinearLayout {
private int[] preDefinedValues;
private int currentProgressIndex;
private SeekBar seekBar;
private int segmentCount = 5:
public SegmentedSeekBar(Context context) {
this(context, null, 0);
}
public SegmentedSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.seekBarStyle);
}
public SegmentedSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.SegmentedSeekBar,
0, 0);
try {
segmentCount =
a.getInt(R.styleable.SegmentedSeekBar_segmentCount, -1);
} finally {
a.recycle();
}
init();
}
public void init() {
//this values will be used when you need to set progress
preDefinedValues = new int[segmentCount];
for(int i = 0; i < preDefinedValues.length; i++) {
preDefinedValues[i] = (100/(segmentCount-1)) * i;
}
//Get layout_5
//which is linearlayout with 6 textviews
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View sliderView = inflater.inflate(
getSliderId(segmentCount), null);
//seekbar already inside the linearlayout
seekBar = (SeekBar)sliderView.findViewById(R.id.seek_bar);
//linear layout is vertically align
//so add your 6 textview linearlayout
addView(sliderView);
seekBar.setOnTouchListener(seekBarTouchListener);
}
private int getSliderId(int size) {
return R.layout.layout_5;
}
//this method sets progress which is seen in UI not actual progress
//It uses the conversion that we did in beginning
public synchronized void setProgress(int progress) {
if(preDefinedValues != null && progress < preDefinedValues.length && progress >= 0) {
seekBar.setProgress(preDefinedValues[progress]);
currentProgressIndex = progress;
}
}
//this listener make sure the right progress is seen in ui
//take action when user finish with changing progress
SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
int index = 0;
for(int i = 0; i < preDefinedValues.length; i++) {
//try to find closest preDefinedvalues by comparing with latest value
if(Math.abs(seekBar.getProgress() - preDefinedValues[i]) < Math.abs(seekBar.getProgress() - preDefinedValues[index])) {
index = i;
}
}
setProgress(index);
}
};
}
Related
Actually I created one custom video player in my app, in this app I'm using SeekBar to show the video progress. Now I'm trying to mark SeekBar with different color at some predefined time index (e.g. 6 Sec, 20 sec and 50 sec), please check below image to understand what exactly I want--
I'm almost done with the marking functionality, but the marking is not getting match with the exact time position. Please check below images to understand my problem--
Image-1]
In this image you can clearly see that the current Thumb position is the exact 6-sec. position and the first Vertical Blue mark is actually my CustomSeekBar marking for 6 sec position.
Image-2]
Same way, in above image you can see that the current Thumb position is the exact 20-sec. position and the second Vertical Blue mark is actually my CustomSeekBar marking for 20-sec position.
Below is my "CustomSeekBar" class --
public class CustomSeekBar extends AppCompatSeekBar
{
private ArrayList<ProgressItem> mProgressItemsList;
public CustomSeekBar(Context context) {
super(context);
mProgressItemsList = new ArrayList<ProgressItem>();
}
public CustomSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomSeekBar(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
public void initData(ArrayList<ProgressItem> progressItemsList)
{
this.mProgressItemsList = progressItemsList;
}
#Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onDraw(Canvas canvas)
{
if (mProgressItemsList!=null && mProgressItemsList.size() > 0)
{
int progressBarWidth = getWidth();
int progressBarHeight = getHeight()+20;
int thumboffset = getThumbOffset()-20;
int lastProgressX = 0;
int progressItemWidth, progressItemRight;
for (int i = 0; i < mProgressItemsList.size(); i++)
{
ProgressItem progressItem = mProgressItemsList.get(i);
Paint progressPaint = new Paint();
progressPaint.setColor(getResources().getColor(
progressItem.color));
progressItemWidth = (int) (progressItem.progressItemPercentage
* progressBarWidth / 100);
progressItemRight = lastProgressX + progressItemWidth;
// for last item give right to progress item to the width
if (i == mProgressItemsList.size() - 1 && progressItemRight != progressBarWidth)
{
progressItemRight = progressBarWidth;
}
Rect progressRect = new Rect();
progressRect.set(lastProgressX, thumboffset / 2,
progressItemRight, progressBarHeight - thumboffset / 2);
canvas.drawRect(progressRect, progressPaint);
lastProgressX = progressItemRight;
}
super.onDraw(canvas);
}
}
}
Below is my ProgressItem class
public class ProgressItem
{
public int color;
public float progressItemPercentage;
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public float getProgressItemPercentage() {
return progressItemPercentage;
}
public void setProgressItemPercentage(float progressItemPercentage) {
this.progressItemPercentage = progressItemPercentage;
}
}
Below is how I'm using it in my VideoActivity--
CustomSeekBar videoProgress = (CustomSeekBar) findViewById(R.id.videoProgress);
// Disable SeekBar Thumb Drag.
videoProgress.setOnTouchListener(new View.OnTouchListener()
{
#Override
public boolean onTouch(View view, MotionEvent motionEvent)
{
return true;
}
});
/*videoProgress.getProgressDrawable().setColorFilter(getResources().getColor(R.color.cerulean_blue), PorterDuff.Mode.SRC_IN);
videoProgress.getThumb().setColorFilter(getResources().getColor(R.color.cerulean_blue), PorterDuff.Mode.SRC_IN);*/
videoProgress.getThumb().setColorFilter(getResources().getColor(R.color.cerulean_blue), PorterDuff.Mode.SRC_IN);
videoProgress.setProgress(0);
videoProgress.setMax(100);
// Function to init markers
ArrayList<ProgressItem> progressItemList;
void initVideoProgressColor()
{
progressItemList = new ArrayList<ProgressItem>();
ProgressItem mProgressItem;
mProgressItem = new ProgressItem();
int vidDuration = vidView.getDuration();
mProgressItem.progressItemPercentage = 6;
Log.e("VideoActivity", mProgressItem.progressItemPercentage + "");
mProgressItem.color = R.color.transparent_clr;
progressItemList.add(mProgressItem);
// FIRST MARKER FOR 6-SEC. POSITION
mProgressItem = new ProgressItem();
mProgressItem.progressItemPercentage = 0.5f;
mProgressItem.color = R.color.cerulean_blue;
progressItemList.add(mProgressItem);
mProgressItem = new ProgressItem();
mProgressItem.progressItemPercentage = 20;
mProgressItem.color = R.color.transparent_clr;
progressItemList.add(mProgressItem);
// SECOND MARKER FOR 20-SEC. POSITION
mProgressItem = new ProgressItem();
mProgressItem.progressItemPercentage = 0.5f;
mProgressItem.color = R.color.cerulean_blue;
progressItemList.add(mProgressItem);
mProgressItem = new ProgressItem();
mProgressItem.progressItemPercentage = 70;
mProgressItem.color = R.color.transparent_clr;
progressItemList.add(mProgressItem);
videoProgress.initData(progressItemList);
videoProgress.invalidate();
}
for more details, please check below link which I refereed to implement this Custom SeekBar-
https://www.androiddevelopersolutions.com/2015/01/android-custom-horizontal-progress-bar.html
Also, I tried solution from below link, but unfortunately getting the same result--
android seek bar customization,
Actually I'm very close to the answer, just need a proper guidance which I think I'll get from you experts. Please let me know if I can provide more details for the same. Thank you.
Finally I got the solution. Below are the steps to implement the solution--
Step-1] Create one "attrs.xml" file in "res/values/" folder and paste below code in that file--
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DottedSeekBar">
<attr name="dots_positions" format="reference"/>
<attr name="dots_drawable" format="reference"/>
</declare-styleable>
</resources>
Step-2] Prepare one image icon which you want to use to mark on progress bar and name it "video_mark.png".
Step-3] Create one custom SeekBar as below--
public class DottedSeekBar extends AppCompatSeekBar {
/** Int values which corresponds to dots */
private int[] mDotsPositions = null;
/** Drawable for dot */
private Bitmap mDotBitmap = null;
public DottedSeekBar(final Context context) {
super(context);
init(null);
}
public DottedSeekBar(final Context context, final AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public DottedSeekBar(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
/**
* Initializes Seek bar extended attributes from xml
*
* #param attributeSet {#link AttributeSet}
*/
private void init(final AttributeSet attributeSet) {
final TypedArray attrsArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.DottedSeekBar, 0, 0);
final int dotsArrayResource = attrsArray.getResourceId(R.styleable.DottedSeekBar_dots_positions, 0);
if (0 != dotsArrayResource) {
mDotsPositions = getResources().getIntArray(dotsArrayResource);
}
final int dotDrawableId = attrsArray.getResourceId(R.styleable.DottedSeekBar_dots_drawable, 0);
if (0 != dotDrawableId) {
mDotBitmap = BitmapFactory.decodeResource(getResources(), dotDrawableId);
}
}
/**
* #param dots to be displayed on this SeekBar
*/
public void setDots(final int[] dots) {
mDotsPositions = dots;
invalidate();
}
/**
* #param dotsResource resource id to be used for dots drawing
*/
public void setDotsDrawable(final int dotsResource)
{
mDotBitmap = BitmapFactory.decodeResource(getResources(), dotsResource);
invalidate();
}
#Override
protected synchronized void onDraw(final Canvas canvas) {
super.onDraw(canvas);
final float width=getMeasuredWidth()-getPaddingLeft()-getPaddingRight();
final float step=width/(float)(getMax());
if (null != mDotsPositions && 0 != mDotsPositions.length && null != mDotBitmap) {
// draw dots if we have ones
for (int position : mDotsPositions) {
canvas.drawBitmap(mDotBitmap, position * step, 0, null);
}
}
}
}
Step-4] Use this custom SeekBar in your activity.xml file as below--
<com.your_package.DottedSeekBar
android:id="#+id/videoProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Step-5] Add below code in "onCreate()" method of your "Activity.java" class--
DottedSeekBar videoProgress = (DottedSeekBar) findViewById(R.id.videoProgress);
// Disable SeekBar Thumb Drag. (Optional)
videoProgress.setOnTouchListener(new View.OnTouchListener()
{
#Override
public boolean onTouch(View view, MotionEvent motionEvent)
{
return true;
}
});
// Set custom thumb icon color here (Optional)
videoProgress.getThumb().setColorFilter(getResources().getColor(R.color.cerulean_blue), PorterDuff.Mode.SRC_IN);
// Add below line to avoid unnecessary SeekBar padding. (Optional)
videoProgress.setPadding(0, 0, 0, 0);
// Handler to update video progress time--
handler = new Handler();
// Define the code block to be executed
final Runnable runnableCode = new Runnable() {
#Override
public void run()
{
updateCurrentTime();
// Repeat this the same runnable code block again another 1 seconds
// 'this' is referencing the Runnable object
handler.postDelayed(this, 1000);
}
};
Use "videoView.setOnPreparedListener()" method to calculate total video time in seconds
yourVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener()
{
#Override
public void onPrepared(MediaPlayer mp)
{
String strTotalDuration = msToTimeConverter(vidView.getDuration());
String[] strTimeArr = strTotalDuration.split(":");
int min = Integer.parseInt(strTimeArr[0]);
int videoLengthInSec = Integer.parseInt(strTimeArr[1]);
videoLengthInSec = videoLengthInSec + (min*60);
videoProgress.setProgress(0);
videoProgress.setMax(videoLengthInSec);
// Start the initial runnable task by posting through the handler
handler.post(runnableCode);
initVideoMarkers();
}
}
);
Step-6] Copy below required methods in your "Activity.java" class--
// Method to update time progress
private void updateCurrentTime()
{
if (videoProgress.getProgress() >= 100)
{
handler.removeMessages(0);
}
String currentPosition = msToTimeConverter(vidView.getCurrentPosition());
String[] strArr = currentPosition.split(":");
int progress = vidView.getCurrentPosition() * videoLengthInSec / vidView.getDuration();
videoProgress.setProgress(progress);
}
// Milliseconds to Time converter Method
String msToTimeConverter(int millis)
{
return String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(millis)),
TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis)));
}
// Method to set Marker values
private void initVideoMarkers()
{
// Here I'm adding markers on 10, 15 and 20 Second index
videoProgress.setDots(new int[] {10, 15, 20});
videoProgress.setDotsDrawable(R.drawable.video_mark);
}
I need an advise on optimizing a custom indicator, that shows progress of downloading file in multiple chunks, in concurrent threads. I couldn't find a correct name for that type - pieces, fragments, chunks? But it should look like bittorrent progress bar or defrag progress from Win XP. And it looks like this:
My custom ProgressBar class as following:
public class FragmentedProgressBar extends ProgressBar {
private int height;
private float fragWidth;
private final ArrayList<Integer> stateColors = new ArrayList<>();
private final Paint progressPaint = new Paint();
private ConcurrentHashMap<Integer, Integer> barData;
public FragmentedProgressBar(Context context) {
super(context);
this.init(context);
}
public FragmentedProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
this.init(context);
}
public FragmentedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.init(context);
}
#SuppressWarnings("deprecation")
private void init(Context context) {
barData = new ConcurrentHashMap<>();
stateColors.addAll(
Arrays.asList(
context.getResources().getColor(android.R.color.holo_blue_dark),
context.getResources().getColor(android.R.color.holo_green_light),
context.getResources().getColor(android.R.color.holo_orange_light),
context.getResources().getColor(android.R.color.holo_red_light)
)
);
}
public synchronized void setProgress(int progress, int state) {
/* state serves to indicate "started", "ready", "retry", "error" by color */
if(barData != null ) {
barData.put(progress, state);
}
super.setProgress(progress);
}
#Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
height = getMeasuredHeight();
setMeasuredDimension(width, height);
fragWidth = (float) width / getMax();
}
#Override
protected synchronized void onDraw(Canvas canvas) {
for (Map.Entry<Integer, Integer> ent : barData.entrySet()) {
int id = ent.getKey();
int state = ent.getValue();
float xleft = fragWidth * ( id - 1 );
progressPaint.setColor(stateColors.get(state));
canvas.drawRect(xleft, 0.0f, xleft + fragWidth, 0.0f + height, progressPaint);
}
}
}
However, in this approach, it redraws whole bar on every progress tick, and, I think, it's quite inefficient.
I've done formerly same bar in javafx, extending Canvas and drawing each chunk separately on it.
What will be a better solution for android, desirably extending and reusing the ProgressBar class?
Thanks
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 trying to make this custom SeekBar in Android 2.2 and everything I do seems to be wrong! I'm trying to display the value of the seekbar over the thumb image of the SeekBar. Does anybody have some experiences with this?
I have followed a different approach which provides more possibilities to customize the thumb. Final output will look like following:
First you have to design the layout which will be set as thumb drawable.
layout_seekbar_thumb.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="#dimen/seekbar_thumb_size"
android:layout_height="#dimen/seekbar_thumb_size"
android:background="#drawable/ic_seekbar_thumb_back"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="#+id/tvProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#000000"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
Here seekbar_thumb_size can be any small size as per your requirement. I have used 30dp here. For background you can use any drawable/icon of your choice.
Now you need this view to be set as thumb drawable so get it with following code:
View thumbView = LayoutInflater.from(YourActivity.this).inflate(R.layout.layout_seekbar_thumb, null, false);
Here I suggest to initialize this view in onCreate() so no need to inflate it again and again.
Now set this view as thumb drawable when seekBar progress is changed. Add the following method in your code:
public Drawable getThumb(int progress) {
((TextView) thumbView.findViewById(R.id.tvProgress)).setText(progress + "");
thumbView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
Bitmap bitmap = Bitmap.createBitmap(thumbView.getMeasuredWidth(), thumbView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
thumbView.layout(0, 0, thumbView.getMeasuredWidth(), thumbView.getMeasuredHeight());
thumbView.draw(canvas);
return new BitmapDrawable(getResources(), bitmap);
}
Now call this method from onProgressChanged().
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// You can have your own calculation for progress
seekBar.setThumb(getThumb(progress));
}
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
Note: Also call getThumb() method when you initialize seekBar to initialize it with default value.
With this approach, you can have any custom view on progress change.
I assume you've already extended the base class, so you have something like:
public class SeekBarHint extends SeekBar {
public SeekBarHint (Context context) {
super(context);
}
public SeekBarHint (Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SeekBarHint (Context context, AttributeSet attrs) {
super(context, attrs);
}
}
Now you override the onDraw method with some of your own code. Insert the following:
#Override
protected void onDraw(Canvas c) {
super.onDraw(c);
}
Now, you want to draw some text near the thumb, but there isn't a convenient way to get the thumb's x-position. We just need a little math.
#Override
protected void onDraw(Canvas c) {
super.onDraw(c);
int thumb_x = ( (double)this.getProgress()/this.getMax() ) * (double)this.getWidth();
int middle = this.getHeight()/2;
// your drawing code here, ie Canvas.drawText();
}
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean b) {
int val = (progress * (seekBar.getWidth() - 2 * seekBar.getThumbOffset())) / seekBar.getMax();
text_seekbar.setText("" + progress);
text_seekbar.setX(seekBar.getX() + val + seekBar.getThumbOffset() / 2);
}
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
text_seekbar.setVisibility(View.VISIBLE);
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
text_seekbar.setVisibility(View.GONE);
}
});
This worked for me
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int val = (progress * (seekBar.getWidth() - 2 * seekBar.getThumbOffset())) / seekBar.getMax();
_testText.setText("" + progress);
_testText.setX(seekBar.getX() + val + seekBar.getThumbOffset() / 2);
}
Hey I found another solution, seems simpler:
private void setText(){
int progress = mSeekBar.getProgress();
int max= mSeekBar.getMax();
int offset = mSeekBar.getThumbOffset();
float percent = ((float)progress)/(float)max;
int width = mSeekBar.getWidth() - 2*offset;
int answer =((int)(width*percent +offset - mText.getWidth()/2));
mText.setX(answer);
}
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
setText();
mText.setText(""+progress);
}
This follow code aligns your TextView center to your SeekBar thumb center.
YOUR_TEXT_VIEW width must be wrap_content in xml.
Hope this code will help you.
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
YOUR_TEXT_VIEW.setText(Integer.toString(progress));
double pourcent = progress / (double) seekBar.getMax();
int offset = seekBar.getThumbOffset();
int seekWidth = seekBar.getWidth();
int val = (int) Math.round(pourcent * (seekWidth - 2 * offset));
int labelWidth = YOUR_TEXT_VIEW.getWidth();
YOUR_TEXT_VIEW.setX(offset + seekBar.getX() + val
- Math.round(pourcent * offset)
- Math.round(pourcent * labelWidth/2));
}
I used this library to create drawable text view and put that drawable into thumb programmatically.
https://github.com/amulyakhare/TextDrawable
Code is something like this:
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
String dynamicText = String.valueOf(progress);
TextDrawable drawable = TextDrawable.builder()
.beginConfig()
.endConfig()
.buildRoundRect(dynamicText , Color.WHITE ,20);
seekBar.setThumb(drawable);
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
Works not bad for me
there is a little hardcode)
please write improvements which smbd may has
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v7.widget.AppCompatSeekBar;
import android.util.AttributeSet;
import android.util.TypedValue;
public class CustomSeekBar extends AppCompatSeekBar {
#SuppressWarnings("unused")
private static final String TAG = CustomSeekBar.class.getSimpleName();
private Paint paint;
private Rect bounds;
public String dimension;
public CustomSeekBar(Context context) {
super(context);
init();
}
public CustomSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setTextSize(sp2px(14));
bounds = new Rect();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String label = String.valueOf(getProgress()) + dimension;
paint.getTextBounds(label, 0, label.length(), bounds);
float x = (float) getProgress() * (getWidth() - 2 * getThumbOffset()) / getMax() +
(1 - (float) getProgress() / getMax()) * bounds.width() / 2 - bounds.width() / 2
+ getThumbOffset() / (label.length() - 1);
canvas.drawText(label, x, paint.getTextSize(), paint);
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
}
IMO best way is to do it through code. It is really not that scary and we are all programmers after all :)
class ThumbDrawable(context: Context) : Drawable() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textBounds = Rect()
private var shadowColor = context.resources.getColor(R.color.wallet_screen_option_shadow)
private val size = context.resources.getDimensionPixelSize(R.dimen.thumbRadius).toFloat()
private val textSize = context.resources.getDimensionPixelSize(R.dimen.thumbTextSize).toFloat()
var progress: Int = 0
init {
textPaint.typeface = Typeface.createFromAsset(context.assets, "font/avenir_heavy.ttf")
val accentColor = context.resources.getColor(R.color.accent)
paint.color = accentColor
textPaint.color = accentColor
textPaint.textSize = textSize
paint.setShadowLayer(size / 2, 0f, 0f, shadowColor)
}
override fun draw(canvas: Canvas) {
Timber.d("bounds: $bounds")
val progressAsString = progress.toString()
canvas.drawCircle(bounds.left.toFloat(), bounds.top.toFloat(), size, paint)
textPaint.getTextBounds(progressAsString, 0, progressAsString.length, textBounds)
//0.6f is cause of the avenirs spacing, should be .5 for proper font
canvas.drawText(progressAsString, bounds.left.toFloat() - textBounds.width() * 0.6f, bounds.top.toFloat() - size * 2, textPaint)
}
override fun setAlpha(alpha: Int) {
}
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
}
and in your seekbar implementation
class CustomSeekBar #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
SeekBar(context, attrs, defStyleAttr) {
init {
thumb = ThumbDrawable(context)
setPadding(0, 0, 0, 0);
}
override fun invalidate() {
super.invalidate()
if (thumb is ThumbDrawable) (thumb as ThumbDrawable).progress = progress
}
}
final result is something like this
I created this example to show how textview should be supported to different types of screen size and how to calculate the real position of Thumb because sometimes the position could be 0.
public class CustomProgressBar extends RelativeLayout implements AppCompatSeekBar.OnSeekBarChangeListener {
#BindView(R.id.userProgressBar)
protected AppCompatSeekBar progressSeekBar;
#BindView(R.id.textPorcent)
protected TextView porcent;
#BindView(R.id.titleIndicator)
protected TextView title;
public CustomProgressBar(Context context) {
super(context);
init();
}
public CustomProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.custom_progressbar_view, this, true);
ButterKnife.bind(this);
setColors(R.color.green, R.color.progress_bar_remaining);
progressSeekBar.setPadding(0, 0, 0, 0);
progressSeekBar.setOnSeekBarChangeListener(this);
}
private void setPorcentTextViewPosition(float widthView) {
int width = CoreUtils.getScreenSize().x;
float xPosition = ((float) progressSeekBar.getProgress() / 100) * width;
float finalPosition = xPosition - (widthView / 2f);
if (width - xPosition < widthView) {
porcent.setX(width - widthView);
} else if (widthView < finalPosition) {
porcent.setX(finalPosition);
}
}
public void setColors(int progressDrawable, int remainingDrawable) {
LayerDrawable layer = (LayerDrawable) progressSeekBar.getProgressDrawable();
Drawable background = layer.getDrawable(0);
Drawable progress = layer.getDrawable(1);
background.setColorFilter(ContextCompat.getColor(getContext(), remainingDrawable), PorterDuff.Mode.SRC_IN);
progress.setColorFilter(ContextCompat.getColor(getContext(), progressDrawable), PorterDuff.Mode.SRC_IN);
}
public void setValues(int progress, int remaining) {
int value = (progress * remaining) / 100;
progressSeekBar.setMax(remaining);
porcent.setText(String.valueOf(value).concat("%"));
porcent.post(new Runnable() {
#Override
public void run() {
setPorcentTextViewPosition(porcent.getWidth());
}
});
progressSeekBar.setProgress(value);
}
public void setTitle(String title) {
this.title.setText(title);
}
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
Add a TextView to your layout. Add onSeekBarChangeListener.
You will want precision so that the text is exactly in the middle of your seek bar thumb, you have to a little calculation. This is because the width of the text is different. Say, you want to show numbers from 0 to 150. Width of 188 will be different from 111. Because of this, the text you are showing will always tilt to some side.
The way to solve it is to measure the width of the text, remove that from the width of the seekbar thumb, divide it by 2, and add that to the result that was given in the accepted answer. Now you would not care about how large the number range. Here is the code:
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
val value = progress * (seekBar.width - 2 * seekBar.thumbOffset) / seekBar.max
label.text = progress.toString()
label.measure(0, 0)
val textWidth = label.measuredWidth
val firstRemainder = seekThumbWidth - textWidth
val result = firstRemainder / 2
label.x = (seekBar.x + value + result)
}
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();
}
}