53

In TextView I set drawableLeft where the drawable is showing from center. I need to align the drawableLeft with top inside TextView like this image.

enter image description here

Is it possible to achieve this?

Vasily Kabunov
  • 6,511
  • 13
  • 49
  • 53
androidcodehunter
  • 21,567
  • 19
  • 47
  • 70
  • 2
    Not simply. There's no `drawableTopLeft`. So, to achieve what you want, either use a separate ImageView or make a new control which extends TextView. – Phantômaxx Sep 01 '14 at 10:24
  • use BitmapDrawable.setGravity method, also make sure your drawable bounds height is equal to textview height – pskink Sep 01 '14 at 10:35
  • @FrankN.Stein you are may be right. Could you please give some elaboration about custom controls. – androidcodehunter Sep 01 '14 at 10:38
  • @Sharif What is your text view height? That have constant height? – Naruto Uzumaki Sep 01 '14 at 10:42
  • @NarutoUzumaki TextView height is not constant. It will grow as text size grow. – androidcodehunter Sep 01 '14 at 10:43
  • 1
    It's a broad argument. Here's how Lars Vogel explains that: http://www.vogella.com/tutorials/AndroidCustomViews/article.html and this is the official reference: http://developer.android.com/guide/topics/ui/custom-components.html – Phantômaxx Sep 01 '14 at 10:46
  • @Sharif if your textview height have constant value like 50dp, you can create a .png file with height=50 and then set your image to above half of .png file(height 25dp above). if NO, you can use from this trick, but with Ninepatch. I hope this be useful. – Naruto Uzumaki Sep 01 '14 at 10:53
  • It's quite simple using just XML, see my answer below. – Andrew Kelly Aug 01 '17 at 02:48

7 Answers7

13

Use SpannableString and ImageSpan to achieve this.

String msg=" "+"haii";
ImageSpan mImageSpan== new ImageSpan(mContext, R.drawable.icon);
SpannableString text = new SpannableString(msg);
text.setSpan(mImageSpan, 0, 1, 0);
mTextView.setText(text);

the extra space in the string variable is replaced by the icon.

Rahul M Mohan
  • 293
  • 1
  • 3
  • 12
  • 2
    Lovely solution. But both icon and text are not on same gravity. Do you have any suggestion? – User Aug 26 '16 at 14:12
  • Try removing fontPadding on TextView. – Rahul Feb 12 '19 at 18:30
  • This is not a generic solution, what if I need drawable at the end, as we do with `setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)` – Choletski Jun 30 '22 at 15:03
10

I think it's even easier than all the answers above: You only need to do this:

public class TopGravityDrawable extends BitmapDrawable {

    public TopGravityDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }

    @Override
    public void draw(Canvas canvas) {
        int halfCanvas = canvas.getHeight() / 2;
        int halfDrawable = getIntrinsicHeight() / 2;
        canvas.save();
        canvas.translate(0, -halfCanvas + halfDrawable);
        super.draw(canvas);
        canvas.restore();
    }
}

And then

 final Bitmap bitmap = BitmapFactory.decodeResource(mTitle.getResources(), R.drawable.icon);
 icon = new TopGravityDrawable(mTitle.getResources(), bitmap);
 title.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);

Remember this only works properly with LEFT and RIGHT compound drawables

Ambran
  • 2,367
  • 4
  • 31
  • 46
cesards
  • 15,882
  • 11
  • 70
  • 65
7

See my answer here.

You can align a compound-Drawable to the top (or bottom) by creating a custom Drawable that wraps your Drawable, and then manipulate the drawing of your custom Drawable by overriding the method onDraw(Canvas).

The sample below is the simplest possible example. This aligns the image to the top, but you can also make it align to the bottom, left or right of the TextView by implementing the required logic in the onDraw(Canvas)-method. You might also want to build in a margin in the onDraw(Canvas), to make your design implementation pixel perfect.

Sample usage:

GravityCompoundDrawable gravityDrawable = new GravityCompoundDrawable(innerDrawable);
// NOTE: next 2 lines are important!
innerDrawable.setBounds(0, 0, innerDrawable.getIntrinsicWidth(), innerDrawable.getIntrinsicHeight());
gravityDrawable.setBounds(0, 0, innerDrawable.getIntrinsicWidth(), innerDrawable.getIntrinsicHeight());
mTextView.setCompoundDrawables(gravityDrawable, null, null, null);

Sample code:

public class GravityCompoundDrawable extends Drawable {

    // inner Drawable
    private final Drawable mDrawable;

    public GravityCompoundDrawable(Drawable drawable) {
        mDrawable = drawable;
    }

    @Override
    public int getIntrinsicWidth() {
        return mDrawable.getIntrinsicWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        return mDrawable.getIntrinsicHeight();
    }

    @Override
    public void draw(Canvas canvas) {
        int halfCanvas= canvas.getHeight() / 2;
        int halfDrawable = mDrawable.getIntrinsicHeight() / 2;

        // align to top
        canvas.save();
        canvas.translate(0, -halfCanvas + halfDrawable);
        mDrawable.draw(canvas);
        canvas.restore();
    }
}
Community
  • 1
  • 1
Reinier
  • 3,836
  • 1
  • 27
  • 28
4

you can do like this:

public class DrawableTopLeftTextView extends TextView {

    public DrawableTopLeftTextView(Context context) {
        super(context);
    }

    public DrawableTopLeftTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public DrawableTopLeftTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        if (!TextUtils.isEmpty(getText())) {

            Drawable[] drawables = getCompoundDrawables();

            if (drawables != null) {

                Drawable drawableLeft = drawables[0];

                if (drawableLeft != null) {

                    Paint.FontMetricsInt fontMetricsInt = getPaint().getFontMetricsInt();
                    Rect bounds = new Rect();
                    getPaint().getTextBounds((String) getText(), 0, length(), bounds);
                    int textVerticalSpace = Math.round(bounds.top - fontMetricsInt.top);
                    int offset = (getHeight() - drawableLeft.getIntrinsicHeight()) / 2 - textVerticalSpace - getPaddingTop() / 2;
                    drawableLeft.setBounds(0, -offset, drawableLeft.getIntrinsicWidth(), drawableLeft.getIntrinsicHeight() - offset);
                }
            }
        }
        super.onDraw(canvas);
    }
}
Kurt Van den Branden
  • 11,995
  • 10
  • 76
  • 85
xzw happy
  • 91
  • 2
3

If you want a purely XML solution then you can use an inset drawable to re-position your desired drawable.

<?xml version="1.0" encoding="utf-8"?>
<inset
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/actual_image_to_be_shown"
    android:insetTop="-32dp" />

You might have to play around with the inset value depending on your scenario. Then just use this XML drawable in your TextView drawableLeft/Start definition.

Andrew Kelly
  • 2,180
  • 2
  • 19
  • 27
  • 3
    If you're going to give me a down vote, at least tell me why you don't like this solution. – Andrew Kelly Jan 18 '18 at 03:42
  • 7
    Hard-coded values don't work with dynamic text. The negative offset depends on how many rows of text there are + the `fontSize` and other properties of the `TextView`. It's a neat hack though for single-line. – TWiStErRob Jan 18 '18 at 17:36
  • Without downvote but maybe the reason for them: Quote: "You might have to play around with the inset value depending on your scenario" => that means for textviews with dynamic length it is not a solution or at least not the best one – Tima Jul 24 '18 at 08:23
0

Your can use by this way. May it can helpful you.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >

<RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="100dp"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/imageView1"
        android:text=" Text Text Text Text Text Text Text Text Text Text Text Text 
        Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text   Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text" />
</RelativeLayout>

androidcodehunter
  • 21,567
  • 19
  • 47
  • 70
Mayur Chudasama
  • 584
  • 4
  • 8
  • 1
    I don't want to follow this way as in this way we have to use nested layout. – androidcodehunter Sep 01 '14 at 10:45
  • 1
    The outer RelativeLayout is completely useless. AND `fill_parent` is deprecated since API Level 8 included. Use `match_parent`, instead – Phantômaxx Sep 01 '14 at 10:48
  • 1
    @androidcodehunter it's either nested layouts or custom code, we need to take some kind of hit :( I always prefer an XML-only solution if it's reasonably good, which this is properly implemented (you can solve it with a simple horizontal `LinearLayout`). By the way you don't even need nesting if you use `ConstraintLayout` or `RelativeLayout`, but it's still 2 views. – TWiStErRob Jan 18 '18 at 17:37
0

Try like this:

class MyTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : AppCompatTextView(context, style) {
    private val leftDrawable = ContextCompat.getDrawable(context, R.drawable.checkmark)

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        setBulletPoint(compoundDrawables[0], canvas)
    }

    private fun setBulletPoint(drawableLeft: Drawable?, canvas: Canvas?) {
        if (!TextUtils.isEmpty(text)) {
            leftDrawable?.let { drlft ->
                if (lineCount == 1) {
                    setCompoundDrawablesWithIntrinsicBounds(drlft, null, null, null)
                } else {
                    val buttonWidth = drlft.intrinsicWidth
                    val buttonHeight = drlft.intrinsicHeight
                    val topSpace = abs(buttonHeight - lineHeight) / 2
           
                    drlft.setBounds(0, topSpace, buttonWidth, topSpace + buttonHeight)
                    canvas?.apply {
                        save()
                        drlft.draw(canvas)
                        restore()
                    }
                }
            }
        }
    }
}
Goran Horia Mihail
  • 3,536
  • 2
  • 29
  • 40