问题
I've almost got this working by following the suggestions in this question
Android percent screen width in RecyclerView item
However that sets a height for all views, same height regardless of the content. In my case I just want to limit how tall an item inside the RecyclerView
can be related to its height.
In my RV I have items that have different heights, with some of those items capable of being even taller than the RV.
My goal is to tell that the item can be at max 60% of the RecyclerView
height and so far the only way I found to achieve this is to set the max height manually whenever a new item is bound to the view holder using this logic:
constraintSet.constrainMaxHeight(viewId, (int) (recyclerView.getMeasuredHeight() * 0.6));
Where recyclerView
is a reference to the hosting RecyclerView
that I pass to the adapter using the adapter's method onAttachedToRecyclerView
and the viewId
is the main view inside the RV item ConstraintLayout
root view with which I control the max height it can take.
Althought this works for what I need, I'd like to find a simpler/more optimized solution for this, similar to this one in terms of simplicity: https://stackoverflow.com/a/51224889/524695
And that means any use of ViewTreeObserver
is excluded.
Any ideas?
回答1:
I'd recommend the same approximate strategy I used in my linked answer: set the max height when you create the ViewHolder
. However, generic View
does not support the maxHeight
property, so we'll leverage ConstraintLayout
to achieve what we want.
If the root view for your item views is already a ConstraintLayout
, great, otherwise you can wrap whatever you do have inside it. In this contrived example, I'm using this layout (the FrameLayout is using 0dp
width and wrap_content
height in the XML):
<androidx.constraintlayout.widget.ConstraintLayout ...>
<FrameLayout ...>
<TextView .../>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
At creation time, I set the max height of the FrameLayout
to be 60% of the parent's height:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(R.layout.recycler_item, parent, false)
val holder = MyViewHolder(itemView)
val params = holder.frame.layoutParams as ConstraintLayout.LayoutParams
params.matchConstraintMaxHeight = (parent.height * 0.6).toInt()
holder.frame.layoutParams = params
return holder
}
回答2:
Overwriting measureChildWithMargins
is the way to go, however to get it actually working you need custom LayoutParams
so you can "carry over" data from your adapter or straight up inflate your desired percentages out of XML.
This layout manager will handle it:
open class PercentLinearLayoutManager : LinearLayoutManager {
constructor(context: Context?) : super(context)
constructor(context: Context?, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
// where we actually force view to be measured based on custom layout params
override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) {
val pp = child.layoutParams as PercentParams
if (pp.maxPercentHeight <= 0.0f)
super.measureChildWithMargins(child, widthUsed, heightUsed)
else {
val widthSpec = getChildMeasureSpec(width, widthMode,
paddingLeft + paddingRight + pp.leftMargin + pp.rightMargin + widthUsed, pp.width,
canScrollHorizontally())
val maxHeight = (height * pp.maxPercentHeight).toInt()
val heightSpec = when (pp.height) {
ViewGroup.LayoutParams.MATCH_PARENT -> View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.EXACTLY)
ViewGroup.LayoutParams.WRAP_CONTENT -> View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST)
else -> View.MeasureSpec.makeMeasureSpec(min(pp.height, maxHeight), View.MeasureSpec.AT_MOST)
}
child.measure(widthSpec, heightSpec)
}
}
// everything below is needed to generate custom params
override fun checkLayoutParams(lp: RecyclerView.LayoutParams) = lp is PercentParams
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return PercentParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
return PercentParams(lp)
}
override fun generateLayoutParams(c: Context, attrs: AttributeSet): RecyclerView.LayoutParams {
return PercentParams(c, attrs)
}
class PercentParams : RecyclerView.LayoutParams {
/** Max percent height of recyclerview this item can have. If height is `match_parent` this size is enforced. */
var maxPercentHeight = 0.0f
constructor(c: Context, attrs: AttributeSet) : super(c, attrs){
val t = c.obtainStyledAttributes(attrs, R.styleable.PercentLinearLayoutManager_Layout)
maxPercentHeight = t.getFloat(R.styleable.PercentLinearLayoutManager_Layout_maxPercentHeight, 0f)
t.recycle()
}
constructor(width: Int, height: Int) : super(width, height)
constructor(source: MarginLayoutParams?) : super(source)
constructor(source: ViewGroup.LayoutParams?) : super(source)
constructor(source: RecyclerView.LayoutParams?) : super(source)
}
}
To handle inflation from XML custom attribute is needed, add it to values/attrs.xml
:
<declare-styleable name="PercentLinearLayoutManager_Layout">
<attr name="maxPercentHeight" format="float"/>
</declare-styleable>
Then you have two options:
1 - alter custom params inside onBindViewHolder
to have per-item control:
val lp = holder.itemView.layoutParams as PercentLinearLayoutManager.PercentParams
if(modifyThisViewHolderHeight) {
lp.maxPercentHeight = 0.6f
} else {
lp.maxPercentHeight = 0.0f // clear percentage in case viewholder is reused
}
2 - use custom attribute inside your viewholder layout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:maxPercentHeight ="0.6">
<!-- rest of layout-->
</FrameLayout>
来源:https://stackoverflow.com/questions/64789096/how-to-set-recyclerview-item-max-height-relative-to-parent