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 andandroid:autoSizeTextType="uniform"
&android:inputType="textMultiLine|textCapSentences"
as attributes orandroidx.appcompat.widget.AppCompatEditText
, accompanied by the attributesapp:autoSizeMaxTextSize="28sp"
,app:autoSizeMinTextSize="8sp"
andapp: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):
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 observe
d 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)
}
}
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 ViewHolder
s (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 ViewHolder
s 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 ViewHolder
s):
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>
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
Answer Checked By - Senaida (JavaFixing Volunteer)