1

Looking for a way to align an Imageview which would be in line with the textview and have it be able to adjust if the textview is too long and will extend to the next line. The reason it is an imageView, is I want it to be clickable

I haven't been successful, I have tried image and text spans and also constraintlayout but I can't seem to the get following result below:

Thanks [1]: https://i.stack.imgur.com/onPT9.png

  • So, the "i" in the circle is the _ImageView_ that you want aligned? Will just the "i" be clickable or the entire _TextView_? – Cheticamp Jan 22 '23 at 23:20
  • 1
    Set an _ImageSpan_ wrapped in a _ClickableSpan_ and set it as the last character in the _TextView_. – Cheticamp Jan 22 '23 at 23:25
  • I've had issues with this method, if we set the ImageSpan to an image, when text is resized or the length is an certain amount, there are scenarios where the ImageSpan is out of the screen bounds and not visible on the screen – keyboard_user Jan 22 '23 at 23:28
  • Also with spans, the accessibility is not great :( – keyboard_user Jan 22 '23 at 23:30
  • I am trying to use baseline alignment with image view but not sure how to deal with textflow to the next line – keyboard_user Jan 22 '23 at 23:30
  • Do you have an example of the spans being outside of screen bounds? What is missing in accessibility. You can do what you want with an _ImageView_, but it would involve some coding. – Cheticamp Jan 22 '23 at 23:33
  • Let me create an example – keyboard_user Jan 22 '23 at 23:37
  • 1
    You can check the following answer https://stackoverflow.com/a/68649759/11604994 – AziKar24 Jan 23 '23 at 03:38

1 Answers1

2

Here is a way to add an image to the end of the text in a TextView whether the text spans one or several lines. The approach is to add a space to the end of each text string and replace that space with an ImageSpan overlaid with a ClickableSpan.

Here is the layout used:

<androidx.appcompat.widget.LinearLayoutCompat
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:background="@android:color/holo_blue_light"
        android:padding="8dp"
        android:text="@string/test_string_1"
        android:textSize="28sp" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:background="@android:color/holo_blue_light"
        android:padding="8dp"
        android:text="@string/test_string_2"
        android:textSize="28sp" />

</androidx.appcompat.widget.LinearLayoutCompat>

And the string resources:

<string name="test_string_1"><b>This</b> is a short string.</string>
<string name="test_string_2"><b>This</b> is some text that spans several lines and is just used as an example.</string>

After waiting for the layout to complete, we can add the images to the end of the text for each TextView.

binding.root.doOnNextLayout {
    // Make the drawables truly clickable.
    binding.textView1.text = addEndImage(binding.textView1)
    binding.textView1.movementMethod = LinkMovementMethod.getInstance()

    binding.textView2.text = addEndImage(binding.textView2)
    binding.textView2.movementMethod = LinkMovementMethod.getInstance()
}

private fun addEndImage(textView: TextView): Spannable {
    // Get out (probable) StaticLayout from the TextView and some of its attributes.
    val size = textView.layout.run {
        val lastLine = lineCount - 1
        -getLineAscent(lastLine) * 2 / 3
    }
    // Get the text and add a space for the spans at the end. If we are certain that the
    // text can be accurately represented by an unspanned String, we could just use
    // "${binding.textView.text} ".toSpannable()
    val text = SpannableStringBuilder(textView.text).append(" ")
    // Get the drawable and size it to fit on our last line.
    val d = AppCompatResources.getDrawable(requireContext(), R.drawable.circle)!!
    d.setBounds(0, 0, size, size)
    // Set the ImageSpan to replace the space we added at the end. Vertical positioning
    // and the size of the image may need to be tweaked.
    val span = ImageSpan(d, ImageSpan.ALIGN_BASELINE)
    text.setSpan(span, text.length - 1, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    // Set the ClickableSpan to overlay the ImageSpan we added at the end.
    val clickableSpan = MyClickableSpan()
    text.setSpan(
        clickableSpan,
        text.length - 1,
        text.length,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )

    return text
}

class MyClickableSpan : ClickableSpan() {
    override fun onClick(widget: View) {
        Toast.makeText(widget.context, "Clicked", Toast.LENGTH_SHORT).show()
    }

}

enter image description here



If you need to use an ImageView for accessibility or other reasons, you can do that as follows.

The layout:

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_light"
        android:padding="8dp"
        android:text="@string/test_string_2"
        android:textSize="28sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:padding="8dp"
        android:src="@drawable/circle"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Again, after layout is complete, we can do the following that will give the ImageView top and left margins that will place it at the end of the text.

    binding.root.doOnNextLayout {
        val imageView = binding.imageView
        imageView.setOnClickListener() {
            Toast.makeText(requireContext(), "Clicked", Toast.LENGTH_SHORT).show()
        }
        val textView = binding.textView2
        val layout = textView.layout
        val imageY = textView.bottom
        val shiftX = layout.getLineRight(layout.lineCount - 1)
        val shiftY =
            -(imageView.y - imageY) - imageView.paddingTop - textView.height + layout.getLineBaseline(
                layout.lineCount - 1
            ) - imageView.height / 2
        val lp = (imageView.layoutParams as ViewGroup.MarginLayoutParams)
        lp.marginStart = shiftX.toInt()
        lp.topMargin = shiftY.toInt()
        imageView.layoutParams = lp
    }

enter image description here

You will have to work with the exact size and placement, but this is a technique that will work.

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • thanks, really appreciate you going through the method. The second method is very promising. Would it be okay, if you could explain the purpose of `-(imageView.y - imageY)` when calculating the margin of Y? Thanks! – keyboard_user Feb 09 '23 at 04:52
  • 1
    `imageView.y` is the top of the _ImageView_ and `imageY` is the bottom of the _TextView_ in the parent's coordinates. `-(imageView.y - imageY)` is the translation needed to place the _ImageView_ aligned to the bottom of the _TextView_. Upon reflection, there are better computations to place the _ImageView_. Why not just move the image by the _TextView's_ height or, better yet, just start with the image's top aligned with the text's bottom? The answer gives the idea but not the best computational solution IMO. – Cheticamp Feb 09 '23 at 14:00