I wish to add a Custom ItemDecoration in my RecyclerView that is a layout defined in a XML file.
So far I was able to inflate the XML and position using canvas.translate (yet, without understanding everything).
Currently I have this code to draw:
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val left = child.marginLeft
val top = child.top
context?.let {
//Inflate the Layout and set the Values (a text in this case)
val view = LayoutInflater.from(it).inflate(R.layout.my_decoration_layout, parent, false)
val textView = view.findViewById<TextView>(R.id.textView)
textView.text = "This is an ItemDecoration"
//Calculate the Size. Im using "hardcoded" values, but this does not seems to change how the View is rendered
view.measure(1000,1000)
view.layout(0, 0, 1000, 1000)
//Draw. I had to translate the canvas to apply the offset for each "ViewHolder"
canvas.save()
canvas.translate(left.toFloat(), top.toFloat())
view.draw(canvas)
canvas.restore()
}
}
}
The XML is (note the background colors to see the Rendered Area):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_red_light"/>
</LinearLayout>
With this, I could inflate and position my XML between each ViewHolder, but I still have two big issues:
My TextView is drawed over the ViewHolder, so I get here an overlap.
Would be great if this "Draw" operation pushes the ViewHolder.
Even my custom XML layout has it's width as "match_parent", it only wraps the text view.
If possible, Id like to know which exactly what "measure" and "layout" means and how it affect my View "area".
And how to prevent the overlap.
I "solved" the Overlap issue drawing a Text on the Canvas, using
Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
textSize = 40f
canvas.drawText(year.toString(), left.toFloat(), top.toFloat(), this)
}
But since my layout is a bit more complex, would be nice to understand how to do it with XML layouts.
Thanks
Related
I am trying to add a custom view (the portion with red boundary will be replaced with my custom view having a 3 dots loader animation) at the end of a multi-line TextView in my Android app.
I know how to specifically add an ImageView using a SpannableString like this:-
fun addImageToEndOfTheString(text: String, drawableResourceId : Int ,context: Context) : SpannableStringBuilder {
val drawable = ContextCompat.getDrawable(context, drawableResourceId)!!
drawable.setBounds(0, 0, 98, 50)
val rocketImageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BASELINE)
val ssBuilder = SpannableStringBuilder(text)
ssBuilder.setSpan(
rocketImageSpan,
text.length-1,
text.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
return ssBuilder
}
But the problem is I have a custom view which I want to place at the end of my TextView. How can I achieve that?
Note : My custom view is a subclass of View.
simply: you can't.
in posted example you aren't adding ImageView, you are adding ImageSpan with just drawable, thats very not the same...
Views can't hold/host other Views, you have to use ViewGroup or some subclass
easiest way would be to make some LinearLayout with vertical orientation and two Views: TextView and your custom View. second one may have android:visibility="gone" and you can show it only when needed
Currently I'm developing a screen which will generate views dynamically based on a response and it involves a lot of cases where I should change the focus/position of scroll view to the selected view. So far I have managed to solve most of issues by using this method when view is not visible on screen:
fun View.requestViewOnScreen() {
val rect = Rect(0, 0, width, height)
requestRectangleOnScreen(rect, false)
}
The problem is that I have a hierarchy of nested views, and when the view is visible, it stays where it is when I call the above code, even if i try it with the code below (also with delay):
scrollView.smoothScrollTo(0, view.top)
Hierarchy is like:
NestedScrollView
ConstraintLayout
CustomView
LinearLayout
The view which I want to position it on the top/center of screen
ScrollView widget:
<androidx.core.widget.NestedScrollView
android:id="#+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="#+id/stickyCtaView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
Any idea how can I adapt my code to make the NestedScrollView to scroll and position the requested view in center? Also I want to note that I have a reference to the view object itself. Thanks in advance!
view.top returns the top position of a view relative to its direct parent, in your case it's the top position in relation to a greater parent is required for that you can use a method like the following to aggregate the top position of a view in relation to the scrollView and scroll to it
private fun ScrollView.scrollTo(target: View) {
var topPosition = 0
var view = target
while (view !== this) {
topPosition += view.top
view = view.parent as View
}
smoothScrollTo(0, topPosition)
}
so to scroll to any view you just call scrollView.scrollTo(view)
I'm developing a music collection app, and I'm struggling with my current goal :Designing a scrollable list of 3 textviews - track number, track title and track duration - which will represent songs in an album. I want the list's layout to be composed of 3 columns with fixed width,aligned as one would expect, without the title textview for a particular track, for example, stretching over above the duration textview of the track below.
I have considered the following options:
Using one RecyclerView with its ViewHolder layout composed of a horizontal LinearLayout of 3 textviews. The problem here is that I don't know how to keep the columns aligned this way. Should I set fixed widths for the textviews using definite dps? How then can I set the entire row to fill the device's entire width?
Using 3 RecyclerViews for each column. I've tried this idea halfway, and the columns are aligned nicely, but scrolling one RV doesn't scroll the others, obviously, and to fix that - as I've seen in another question here - I'll need to mess with the scrolling mechanism, and it seems it can still be very prone to errors with the RVs still might manage to get out of sync or the app crashing.
I know there is GridLayout (which I'm not familiar with and don't know if it can solve my problem), but since it's now considered legacy, I would rather refrain from using it.
Above all, I would like to know what way is considered "best practice", assuming there is one, to tackling such a problem.
Thanks!
This is an alternative solution inspired by Farshad Tahmasbi's answer I found here.
In this screenshot you can see the result. (The duration column is empty right now).
mTracksRecyclerView.setAdapter(new TrackListAdapter(getContext(), musicItem.getTracks()));
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 12);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
int type = position % 3 ;
if (type == 0) {
// Position
return 2;
} else if (type == 1) {
// Title
return 8;
} else {
// Duration
return 2;
}
}
});
mTracksRecyclerView.setLayoutManager(gridLayoutManager);
I tried to create a scrollable list of 3 TextViews for a row.
Like you have mentioned, we can use a RecyclerView as the scrollable list.
activity_main.xml
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ConstraintLayout>
For a list item to have 3 TextViews in a row, I used a GridLayout as follows.
list_item.xml
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="1" >
<TextView
android:id="#+id/trackNumber"
android:layout_columnWeight="1"
android:layout_width="0dp"
android:background="#android:color/holo_red_light" />
<TextView
android:id="#+id/trackTitle"
android:layout_columnWeight="6"
android:layout_width="0dp"
android:ellipsize="end"
android:background="#android:color/holo_green_light" />
<TextView
android:id="#+id/trackDuration"
android:layout_columnWeight="1"
android:layout_width="0dp"
android:background="#android:color/holo_blue_light" />
</GridLayout>
For each TextView a layout_columnWeight value is set to specify the relative proportion of horizontal space that should be allocated for it.
Each TextView's layout_width value is set to 0dp for it to take full width of the allocated space.
But instead of GridLayout, we can use a ConstraintLayout and achieve the same result.
As mentioned above in the question, GridLayout is marked as legacy.
It better to use ConstraintLayout.
list_item.xml
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/trackNumber"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
android:background="#android:color/holo_red_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#id/trackTitle"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/trackTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="6"
android:ellipsize="end"
android:background="#android:color/holo_green_light"
app:layout_constraintStart_toEndOf="#id/trackNumber"
app:layout_constraintEnd_toStartOf="#id/trackDuration"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/trackDuration"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
android:background="#android:color/holo_blue_light"
app:layout_constraintStart_toEndOf="#id/trackTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
For each TextView a layout_constraintHorizontal_weight value is set to specify the relative proportion of horizontal space that should be allocated for it.
Each TextView's layout_width value is set to 0dp for it to take full width of the allocated space.
This is just a Kotlin class to store track information.
class TrackInfo(
var trackNumber: Int,
var trackTitle: String?,
var durationMinutes: Short,
var durationSeconds: Short
)
This is the Adapter class and ViewHolder class for the RecyclerView.
class TrackInfoAdapter(private val items: List<TrackInfo>, private val mContext: Context) :
RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false)
)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val trackInfo = items[position]
holder.trackNumber.text = trackInfo.trackNumber.toString()
holder.trackTitle.text = trackInfo.trackTitle
// Use String.format() to add leading zero for single digit durationSeconds value
val formattedDurationSeconds = "%02d".format(trackInfo.durationSeconds)
holder.trackDuration.text = "${trackInfo.durationMinutes}:$formattedDurationSeconds"
}
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val trackNumber = view.trackNumber
val trackTitle = view.trackTitle
val trackDuration = view.trackDuration
}
This is the MainActivity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Prepare list of track information
val trackInfoList = mutableListOf<TrackInfo>().apply {
// track 1
add(
TrackInfo(1, "Lady GaGa - Poker Face",
4, 4)
)
// track 2
add(
TrackInfo(2, "T.I. featuring Rihanna - Live Your Life",
4, 1)
)
// track 3
add(
TrackInfo(3, "Kanye West - Stronger",
5, 11)
)
// track 4
add(
TrackInfo(4, "Leon Haywood - I Wanna Do Something Freaky To You",
6, 0)
)
// track 5
add(
TrackInfo(5, "Hilary Duff - Reach Out",
4, 16)
)
}
recyclerView.layoutManager = LinearLayoutManager(baseContext)
// Set track information list to Adapter of RecyclerView
recyclerView.adapter = TrackInfoAdapter(trackInfoList, baseContext)
}
}
Result :
I hope this answers the first part of your question.
You have mentioned about 'stretching over above the duration textview, when there's a particular track without a title'.
For that, children of a GridLayout can be configured to span multiple cells using android:layout_columnSpan attribute.
But to handle trackTitle null cases and set properties like layout_columnSpan of TextViews accordingly, I think it would be better to write a custom view class for list item.
the best solution to this is using RecyclerView.
1 - you can set the width of the TextViews to match_parent if you want them to fill the width of the screen.
2 - you need only one recycler view that shows a list of a view.
if you can show me a picture of what you want to achieve
So, this should be pretty basic, but I am new to Kotlin. So I basically have the following ImageView inside a RelativeLayout view.
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="#drawable/moon_0"
android:id="#+id/imagen_luna"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" />
The thing is that under certain condition this ImageView can collide visually with another element, so I have to modify the marginStart and the marginEnd properties. I saw in some tutorials that first I must obtain the current layout margins or something like that, but I am not sure why it's so complicated, in Swift I just modify the properties of each margin.
Is there an easy way to achieve this or in case there isn't what would be the easiest way to do this?
UPDATE - Added code in the Adapter
This is the code I have in my adapter for this specific ImageView:
override fun onBindViewHolder(holder: AnoViewHolder, position: Int) {
...
if(respec.nombre_icono_signo != "" && respec.imagen_luna != "") {
val layoutParamsLuna = holder.view?.imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParamsLuna.setMargins(1, 8, 15, 8)
holder.view?.imagen_luna.layoutParams = layoutParamsLuna
}
...
}
The thing is that this does nothing and my theory is that the setMargins function accepts left and right margins, not start and end margins.
You can update margin in layoutParams of view
Ex:
val layoutParams= imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParams.marginEnd=resources.getDimension(your_dimension).toInt()
layoutParams.marginStart=resources.getDimension(your_dimension).toInt()
layoutParams.bottomMargin=resources.getDimension(your_dimension).toInt()
layoutParams.topMargin=resources.getDimension(your_dimension).toInt()
//or
layoutParams.setMargins(start,top,end,bottom)
imagen_luna.layoutParams=layoutParams
You have to add value for margins in dimension file.because if you set setMargins(1, 8, 15, 8) like this,It's will margin in pixel not in dp.
So use margin values like below.
<dimen name="spacing_normal">16dp</dimen>
<dimen name="spacing_small">8dp</dimen>
<dimen name="spacing_tiny">4dp</dimen>
Update your dapter class like belo
override fun onBindViewHolder(holder: AnoViewHolder, position: Int) {
...
if(respec.nombre_icono_signo != "" && respec.imagen_luna != "") {
val layoutParamsLuna = holder.view?.imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParamsLuna.setMargins(context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt())
holder.view?.imagen_luna.layoutParams = layoutParamsLuna
}else{
//reset margin for else case because recylerview reuse views so it's will keep previous states
}
...
}
I'm currently working on animation that will grow up the view if the user clicks it. Basically, its a card that, when clicked, it will reveal the bottom content. For that, I'm extending Animation like this:
Val collapseAnimation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
val interpolatedInverted = 1 - interpolatedTime
val headerLp = headerImage.layoutParams
headerLp.width = ...
headerImage.layoutParams = headerLp
}
}
The problem is that i need to get the height of a view (wrap_content) that is defined in XML as 0dp. Basically, I want to grow up a view from 0dp to wrap_content and for that i need to know what is the wrap_content size.
How can I accomplish that in the most efficient way, without hard coding the view size?
In order to measure a view with different layout params and get its height, we can do the following:
contentContainer.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
val contentContainerFinalHeight = contentContainer.measuredHeight