Android Studio: autoSize of EditText doesn't work

Issue

in my current project there’s an EditText with fixed layout_width and layout_height, called exercise that is expanded downwards programmatically: One line of text (String) + "\n" is added to the EditText.

Sometimes the line added to the EditText, let’s call it element, is too long to fit inside the full width of the object so it’s splitted into a new line.
The thing is I would either like the lines’ text size in exercise to be resized to fit the EditText’s width or have a clear visible distance between each line (element), but just not inside the newline due to not fitting inside the exercise‘s width.

Therefore I googled as much as I could and tried out every possible solution I could find today.
What I tried:

  • Using either EditText as the object and android:autoSizeTextType="uniform" & android:inputType="textMultiLine|textCapSentences"as attributes or androidx.appcompat.widget.AppCompatEditText, accompanied by the attributes app:autoSizeMaxTextSize="28sp", app:autoSizeMinTextSize="8sp"and app:autoSizeStepGranularity="1sp"
    (with a device that supports just exactly API 26)
  • using other types of text objects
  • using lineSpacingExtra to insert some spacing. This unfortunately also inserted the spacing between the wrapped/ splitted line so the original element’s line that got splitted by wrapping inside the EditText had the spacing aswell.

That’s where I am now. I can’t get the text size be reduced automatically when the line would be too wide for the EditText’s width.

I could supply the full XML, if needed.

I’m grateful for any hint that could help here. Thanks in advance!

Solution

Here’s a really basic RecyclerView implementation (using view binding, let me know if you’re not familiar with that – you can just findViewById all the things instead):

A picture of the result, with multiple lines at different sizes

class MainFragment : Fragment(R.layout.fragment_main) {

    lateinit var binding: FragmentMainBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentMainBinding.bind(view)
        with(binding) {
            val adapter = MyAdapter()
            recyclerView.adapter = adapter
            recyclerView.layoutManager = LinearLayoutManager(requireContext())

            addButton.setOnClickListener {
                val item = textEntry.text.toString()
                if (item.isNotBlank()) {
                    adapter.addItem(item)
                    textEntry.text.clear()
                }
            }
        }
    }


}

class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {

    private var data: List<String> = emptyList()

    fun addItem(item: String) {
        data = data + item
        notifyItemInserted(data.lastIndex)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.binding.textView.text = data[position]
    }

    override fun getItemCount(): Int = data.size

    class ViewHolder(val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root)
}
fragment_main.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="10dp">

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginBottom="8dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@id/textEntry"
    />

    <EditText
        android:id="@+id/textEntry"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:singleLine="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/addButton"
        />

    <Button
        android:id="@+id/addButton"
        android:text="ADD"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />


</androidx.constraintlayout.widget.ConstraintLayout>
item_view.xml


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingHorizontal="16dp"
    >
    <TextView
        android:id="@+id/textView"
        app:autoSizeTextType="uniform"
        android:layout_width="match_parent"
        android:layout_height="48sp"
        android:maxLines="1"
        android:gravity="center_vertical"
        />

</FrameLayout>

It’s pretty simple – you have a text entry field and a button to add the contents as a new line. The button passes the contents to addItem on the adapter, which appends it to the list of lines in data. The RecyclerView just displays all the items in data, using a ViewHolder layout that has an auto-sizing TextView to scale each item.

Ideally you’d want to persist data somehow (e.g. the Add button passes the new data to a ViewModel, stores it somehow, updates the current list which the adapter has observed so it updates whenever there’s a change) – I just left it as a basic proof of concept. Also, it’s easier to store separate items if they’re kept separate – you can always serialise it by joining them into a single string if you really want! But generally you wouldn’t want to do that


edit – since you’re having trouble with the setTypeface thing, this is all it is:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    with(holder.binding.textView) {
        val styled = position % 2 == 0
        text = data[position]
        setTypeface(typeface, if (styled) Typeface.BOLD else Typeface.NORMAL)
        setTextColor(if (styled) Color.RED else Color.BLACK)
    }
}

An example of the RecyclerView with alternate items styled differently

The logic is just styling alternate items differently, but hopefully you get the idea. You decide how to style a given item depending on what it is, and then you apply that style by setting attributes as appropriate. It’s always an "if this is true do A, otherwise do B" situation, so you’re always setting the attribute one way or the other. You never only set it for one case, because then you’re leaving old state displayed if it’s not that case.


It’s more complicated, but you also have the option of creating different ViewHolders (with their own XML layouts) for different kinds of item. So instead of having a single ViewHolder that has to work with everything, where you have to reconfigure things like all the styling in onBindViewHolder depending on which type of item is displayed, you can just have different ViewHolders with different styling, different layouts etc:

// creating a sealed class so we can say our adapter handles a MyViewHolder type,
// and we can have a specific set of possible subclasses of that
sealed class MyViewHolder(view: View) : RecyclerView.ViewHolder(view)
class HeaderViewHolder(val binding: HeaderItemBinding) : MyViewHolder(binding.root)
class ItemViewHolder(val binding: ItemViewBinding) : MyViewHolder(binding.root)

// the Adapter now uses the MyViewHolder type (which as above, covers a couple of different
// ViewHolder classes we're using)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {

    private var data: List<String> = emptyList()

    fun addItem(item: String) {
        data = data + item
        notifyItemInserted(data.lastIndex)
    }

    // some identifiers for the different ViewHolder types we're using
    private val HEADER_TYPE = 0
    private val ITEM_TYPE = 1

    override fun getItemViewType(position: Int): Int {
        // Work out what kind of ViewHolder the item in this position should display in.
        // This gets passed to onCreateViewHolder, where you create the appropriate type,
        // and that type of ViewHolder is what gets passed into onBindViewHolder for this position
        return if (data[position].startsWith("Section")) HEADER_TYPE else ITEM_TYPE
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        // creating the appropriate ViewHolder instance depending on the type requested
        return when(viewType) {
            HEADER_TYPE -> HeaderViewHolder(HeaderItemBinding.inflate(inflater, parent, false))
            ITEM_TYPE -> ItemViewHolder(ItemViewBinding.inflate(inflater, parent, false))
            else -> throw RuntimeException("Unhanded view type!")
        }
    }
    
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        // The type of MyViewHolder passed in here depends on what getItemViewType returns
        // for this position - binding is a different type in each case,
        // because it's been generated from different layouts
        when(holder) {
            is HeaderViewHolder -> holder.binding.textView.text = data[position]
            is ItemViewHolder -> holder.binding.textView.text = data[position]
        }
    }

    override fun getItemCount(): Int = data.size

}

(You could be a bit more clever than this, but just to illustrate the general idea!)

That’s using the same item_view.xml as before, and a header_item.xml variation on that (but you could have literally anything, they’re completely separate layouts, completely separate ViewHolders):

header_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingHorizontal="16dp"
    >
    <TextView
        android:id="@+id/textView"
        app:autoSizeTextType="uniform"
        android:layout_width="match_parent"
        android:layout_height="48sp"
        android:layout_weight="1"
        android:textStyle="bold"
        android:textColor="#DD1100"
        android:maxLines="1"
        android:gravity="center_vertical"
        />

</LinearLayout>

An example of using different ViewHolders with their own independent layouts

So instead of having to "redesign" one ViewHolder in code to go back and forth between different item types and their styling, You can just use two differently-styled layouts. It’s a bit more work to set up, but it can be neater and much more flexible when you have completely independent things – especially if you want to give them different functionality. It depends whether it’s worth it to you, or if you’re happy to just have a single ViewHolder and restyle things in code, hide or show elements etc.

Answered By – cactustictacs

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published