Search code examples
androidtextviewimageview

Having an imageview right aligned to a textview


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.sstatic.net/onPT9.png


Solution

  • 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.