GridView with last item centered - android

I'm doing a GridView (not dynamic), that contains 13 values and I want to put it on two columns and it looks like :
And this is my code :
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvCard"
android:text="A"
android:layout_marginRight="#dimen/_7sdp"
android:textSize="#dimen/_27sdp"
android:layout_centerVertical="true"
android:layout_toLeftOf="#+id/ivCard"
android:layout_toStartOf="#+id/ivCard" />
<ImageView
android:id="#+id/ivCard"
android:layout_width="#dimen/_20sdp"
android:layout_height="#dimen/_28sdp"
android:src="#mipmap/ic_launcher"
android:scaleType="centerCrop"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
And I used Span why isn't putting it on the middle?
GridLayoutManager recycleLayoutManager = new GridLayoutManager(mContext, 2,GridLayoutManager.VERTICAL, false);
recycleLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
// grid items to take 1 column
if (mList.size() % 2 == 0) {
return 1;
} else {
return (position == mList.size()-1) ? 2 : 1;
}
}
});

Here it is a sample project implementing exactly your scenario.
Basically you have to wrap your cards with a layout having width to be defined programmatically in onCreateViewHolder method of the adapter.
In a 2 columns grid, width will be half of parent.

I faced the same problem : I have a GridLayout with two columns and an odd number of elements. The last element must be centered.
I solved it using a custom SpanSizeLookup and putting some margins on the last element :
In the RecyclerView / Activity :
val layoutManager = GridLayoutManager(context, 2)
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup()
{
override fun getSpanSize(position: Int): Int
{
return if (position == 0 || isLastRowAndShouldBeCentered(position)) 2 else 1
}
}
myRecyclerView.layoutManager = layoutManager
And in the Adapter :
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder
{
// Save the width of the container
this.containerWidth = parent.measuredWidth
// Create your ViewHolder here
}
override fun onBindViewHolder(holder: ViewHolder, position: Int)
{
...
/**
* This margin will be exactly 1/4 of the container width.
* This way, the element will be displayed as follows :
* [ BLANK 1/4 SPACE ] [ ELEMENT 1/2 WIDTH ] [ BLANK 1/4 SPACE ]
*/
if (isLastRowAndShouldBeCentered(position))
{
val params = viewHolder.view.layoutParams as GridLayoutManager.LayoutParams
val marginSize = this.containerWidth / 4 // Re-use the container width here
params.leftMargin = marginSize
params.rightMargin = marginSize
viewHolder.view.layoutParams = params
}
}
Obviously, isLastRowAndShouldBeCentered is yours to implement (returns a Boolean indicating whether we are on the last element and we have an odd number of elements)
Hope it can help someone !

Related

How to put items in center of Grid Layout in Recycler View

I am using GridLayoutManager for RecyclerView.
There are conditions where i need to draw 1, 2, 3 or 4 items per row. Like following.
I am able to achieve this by doing following.
//RecyclerView
override fun getItemViewType(position: Int): Int {
if(mDataList?.get(position)?.totalColumns == 5){
return COLUMNS_5
}
else if(mDataList?.get(position)?.totalColumns == 4){
return COLUMNS_4
}
}
On Fragment class
mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
//If there are 6 items in 1 row, then return 1
switch(homeTabLayoutRecyclerAdapter.getItemViewType(position)){
case COLUMNS_5:
return 5;
case COLUMNS_4:
return 4;
The issue is, i am able to get the layout, i shared above, but i need to center all of the items. Like following:
Following is my recyclerview code.
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvItems"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/guideline32" />

Android RecyclerView ItemDecorator: align Inset Divider with List Item TextView

I would like to inset a RecyclerView ItemDecorator divider to left align with a TextView (the item title) inside the list element that is constrained to be 72dp from the parent left side. Something like we see here in the Google Material Design Docs. I assume that I need to somehow reference the layout params on the titleTextView, but am not sure how to do that from my ItemDecorator code, as I seem to be able to only get the params which is the ConstraintLayout, and not the textview itself. Any help or pointers in the right direction would be greatly appreciated! The relevant ItemDecorator code, where I try to get the textView param looks something like this:
for (int i = 0; i < recyclerView.getChildCount() - 1; ++i) {
View child = recyclerView.getChildAt(i);
RecyclerView.LayoutParams params =(RecyclerView.LayoutParams) child.getLayoutParams();
//this just gives me the constraint layout, not the TextView params, how to reference the textViewParams!?
The RecyclerView looks like this:
<android.support.v7.widget.RecyclerView
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/myList"
android:name="com.example.Something"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="LinearLayoutManager"
tools:context=".controllers.fragments.ExampleFragment"
tools:listitem="#layout/fragment_something"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
The list item xml is a constraint layout with some TextViews, and the relevant portion looks like this:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="72dp">
<ImageView
android:id="#+id/myImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="16dp"
app:layout_constraintLeft_toLeftOf="parent"
tools:src="#drawable/ic_menu_manage"
android:src="#drawable/ic_menu_manage"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp" />
<TextView
android:id="#+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="72dp"
android:layout_marginTop="20dp"
android:text="MyTitle"
android:textAppearance="#style/TextAppearance.AppCompat.Subhead"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#+id/textView5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="SampleTitle" />
...
</android.support.constraint.ConstraintLayout>
use a custom item decorator.
class CustomItemDecoration(context: Context) : RecyclerView.ItemDecoration()
{
private val mDivider: Drawable = context.resources.getDrawable(R.drawable.custom_item_divider) // this is a shape that i want to use
override fun onDrawOver(c: Canvas,
parent: RecyclerView,
state: RecyclerView.State)
{
val left = parent.paddingLeft // change to how much padding u want to add here .
val right = parent.width - parent.paddingRight
val childCount = parent.childCount
for (i in 0 until childCount)
{
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + mDivider.intrinsicHeight
mDivider.setBounds(left, top, right, bottom)
mDivider.draw(c)
}
}
}
you set it like that
recyclerView.addItemDecoration(CustomItemDecoration(context))
Since the ConstraintLayout is a ViewGroup, it is possible to access the child of the ConstraintLayout from inside of the ItemDecorator by simply doing something like:
public class MyItemDivider extends RecyclerView.ItemDecoration {
....some code here
#Override
public void onDrawOver(Canvas canvas, RecyclerView recyclerView,
RecyclerView.State state) {
super.onDrawOver(canvas, recyclerView, state);
for (int i = 0; i < recyclerView.getChildCount() - 1; ++i) {
ViewGroup item = (ViewGroup) recyclerView.getChildAt(i);
//for now the titleText is at index 1 of constraint layout
TextView titleChild = (TextView) item.getChildAt(1);
int titleTextLeftMargin = titleChild.getLeft();
//this would probably be less brittle
//but I would need to somehow change the context for this to be doable?
//View titleChild = item.findViewById(R.id.contractListProductNameTextView);
...some more code here
divider.setBounds(titleTextLeftMargin, top, right, bottom);
divider.draw(canvas);
}
}
}
Since you already know by how much you have to inset the divider from the left side (calculate it from your layout if you don't know), all you need to do is convert that dp value to px as shown here and set it as the left bound in your custom ItemDecoration.
public class InsetDividerDecoration extends RecyclerView.ItemDecoration {
private final int insetValueInDp = 72; // in your case
#Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
int dividerLeft = (int) (insetValueInDp * Resources.getSystem().getDisplayMetrics().density);
...
}
}
And use it in your recycler view -
recyclerView.addItemDecoration(new InsetDividerDecoration());

Make width of horizontal recycler view items 70% of screen width

I am trying to create this effect
I am using a recycler view but my issue, is that each card is 100% of the width of the screen as apposed to 70%.
Here is the xml code for each item
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rowLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/scale_20dp">
<LinearLayout
android:id="#+id/button_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<android.support.v7.widget.CardView
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/currentYear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#drawable/paymentscreengrey"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="35dp"
android:paddingTop="#dimen/scale_50dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/scale_20dp"
android:text="**** **** **** 5432"
android:textColor="#color/white"
android:textSize="#dimen/scale_20dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="2345"
android:textColor="#color/white"
android:textSize="#dimen/scale_16dp" />
If you want the first item to be left-aligned (i.e., reproduce what's in the screenshot), subclass LinearLayoutManager and override the three generate*LayoutParams methods. Here's how I did it: https://gist.github.com/bolot/6f1838d29d5b8a87b5fcadbeb53fb6f0.
class PeekingLinearLayoutManager : LinearLayoutManager {
#JvmOverloads
constructor(context: Context?, #RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL, reverseLayout: Boolean = false) : super(context, orientation, reverseLayout)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
override fun generateDefaultLayoutParams() =
scaledLayoutParams(super.generateDefaultLayoutParams())
override fun generateLayoutParams(lp: ViewGroup.LayoutParams?) =
scaledLayoutParams(super.generateLayoutParams(lp))
override fun generateLayoutParams(c: Context?, attrs: AttributeSet?) =
scaledLayoutParams(super.generateLayoutParams(c, attrs))
private fun scaledLayoutParams(layoutParams: RecyclerView.LayoutParams) =
layoutParams.apply {
when(orientation) {
HORIZONTAL -> width = (horizontalSpace * ratio).toInt()
VERTICAL -> height = (verticalSpace * ratio).toInt()
}
}
private val horizontalSpace get() = width - paddingStart - paddingEnd
private val verticalSpace get() = height - paddingTop - paddingBottom
private val ratio = 0.9f // change to 0.7f for 70%
}
This solution is based on/inspired by the spanning (i.e., fit all items to screen) linear layout manager https://gist.github.com/heinrichreimer/39f9d2f9023a184d96f8.
Btw, if you just want to show the items to the left and to the right, while the current item is centered, you can add padding to the recycler view and set clipToPadding to false. In this case you don't even need a custom layout manager.
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">
I needed to have the items in my horizontal RecyclerView to be 70% of the width of the recyclerview. It can be easily done in onCreateViewHolder():
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row, parent, false);
view.layoutParams = ViewGroup.LayoutParams((parent.width * 0.7).toInt(),ViewGroup.LayoutParams.MATCH_PARENT)
return ViewHolder(view);
}
Two ways of doing this really.
1)Use a custom view for your the recycled view. Override onMeasure to return its width as 70 percent of the screen.
2)In your Recycler View adapter, when you create the view set its width to be 70 percent of the screen's width.
In either case you get the screen size from the Display and just multiply the width by .7. In the first case you set that as the EXACT measured width, in the second you set it in the layout param. The second is probably a bit easier.
I had a similar issue where I had a fragment with a horizontal RecyclerView and wanted each view item's width to be a third of the user's screen. Solved it by adding the following in the constructor of our ViewHolder class (in my case I wanted each view holder to be 1/3 of the screen instead of 70%):
private class OurViewHolder extends RecyclerView.ViewHolder {
...
public OurViewHolder (LayoutInflater inflater, ViewGroup parent) {
super (inflater.inflate (R.layout.list_item_layout, parent, false));
// This code is used to get the screen dimensions of the user's device
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
// Set the ViewHolder width to be a third of the screen size, and height to wrap content
itemView.setLayoutParams(new RecyclerView.LayoutParams(width/3, RecyclerView.LayoutParams.WRAP_CONTENT));
...
}
...
for your row.xml parent, you must use wrap_content for its width then add this property.
android:paddingLeft="25dp"
You will get same result
Just updated a bit from this answer by adding a margin and getting displayMetrics from the resources to make it clean:
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(product: StyleMaster) = with(itemView) {
val width = context.resources.displayMetrics?.widthPixels
if (width != null) {
val params = RecyclerView.LayoutParams(width / 2, RecyclerView.LayoutParams.WRAP_CONTENT)
params.setMargins(2, 2, 2, 2)
itemView.layoutParams = params
}
// Rest of the code goes here...
Try changing LinearLayout with PercentRelativeLayout to "wrap" the RecyclerView and then set the its width to 70%.
Change this:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/rv"
/>
</LinearLayout>
With this:
<android.support.percent.PercentRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_widthPercent="70%"
android:id="#+id/rv"
/>
EDIT
Since Percent Support Library comes along with Android Support Library 23 so please make sure that you update Android Support Library in SDK Manager to the latest version already (actually 24). And then add a dependency like below in build.gradle file:
compile 'com.android.support:percent:24.0.0'

ExpandableListView expandable list not expanding when its height is wrap_content, and something is below it

Here is my layout code. When i click on the expandable list view it does not expand. However, if I increase the height of list it expands. Is there a way to increase the height of list dynamically through code? Can i make it to expand in the middle of my layout. Because if I increase the height, it'll just add unnecessary white space in collapsed list.
Code:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.pocketcash.pocketcash.HomeActivity">
<Switch
android:id="#+id/detailedCashInfoSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:onClick="cashToggleSwitchClicked"
android:text="Toggle Detailed Cash Info" />
<Button
android:id="#+id/addCashButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/expandableListView2"
android:layout_centerHorizontal="true"
android:onClick="launchAddCash"
android:text="Add cash" />
<Button
android:id="#+id/addExpenditureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/addCashButton"
android:layout_centerHorizontal="true"
android:layout_marginTop="25dp"
android:onClick="launchAddExpenditure"
android:text="Add expenditure" />
<TextView
android:id="#+id/recentLogsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/addExpenditureButton"
android:layout_marginTop="50dp"
android:text="Recent Logs:"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="#+id/logText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/recentLogsText"
android:layout_marginTop="0dp"
android:text="Small Text"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="#+id/recentViewAllText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/recentLogsText"
android:layout_marginLeft="10dp"
android:layout_toEndOf="#+id/recentLogsText"
android:layout_toRightOf="#+id/recentLogsText"
android:onClick="recentViewLogClicked"
android:text="(view all)"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="#+id/addViewAllText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/addCashButton"
android:layout_centerHorizontal="true"
android:onClick="addViewLogClicked"
android:text="(view logs)"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="#+id/expenseViewAllText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/addExpenditureButton"
android:layout_centerHorizontal="true"
android:onClick="expenseViewLogClicked"
android:text="(view logs)"
android:textAppearance="?android:attr/textAppearanceSmall" />
<ImageView
android:id="#+id/userImage"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/detailedCashInfoSwitch"
android:layout_marginTop="34dp"
android:background="#drawable/default_user" />
<TextView
android:id="#+id/nameText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/userImage"
android:layout_marginLeft="25dp"
android:layout_toEndOf="#+id/userImage"
android:layout_toRightOf="#+id/userImage"
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="#+id/logoutText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="#+id/nameText"
android:layout_alignStart="#+id/nameText"
android:layout_below="#+id/nameText"
android:clickable="true"
android:onClick="logUserOut"
android:text="logout"
android:textAppearance="?android:attr/textAppearanceSmall" />
<ImageButton
android:id="#+id/settingsButton"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignTop="#+id/nameText"
android:layout_marginLeft="25dp"
android:layout_toEndOf="#+id/nameText"
android:layout_toRightOf="#+id/nameText"
android:background="#drawable/settingsicon"
android:onClick="launchSettingsActivity" />
<fragment
android:id="#+id/fragment_currentBalanceInfo"
android:name="com.pocketcash.pocketcash.CurrentBalanceInfoFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/userImage"
android:layout_centerHorizontal="true"
tools:layout="#layout/fragment_current_balance_info" />
<ExpandableListView
android:id="#+id/expandableListView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/fragment_currentBalanceInfo"
android:layout_centerHorizontal="true"
android:clickable="true" />
</RelativeLayout>
</ScrollView>
After much research on this topic, i could only reach the conclusion that a scrollable object like the expandable listview cant be declared within another scrollable view.
However i tweaked around with my java code a little bit for solving this issue during runtime.
expListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int groupPosition) {
int height = 0;
for (int i = 0; i < expListView.getChildCount(); i++) {
height += expListView.getChildAt(i).getMeasuredHeight();
height += expListView.getDividerHeight();
}
expListView.getLayoutParams().height = (height+6)*10;
}
});
// Listview Group collapsed listener
expListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
#Override
public void onGroupCollapse(int groupPosition) {
expListView.getLayoutParams().height = 61;
}
});
This way i adjusted the height of the widget dynamically when it was expanded or collapsed. Its not a dependable method and it works on hit and trial so i ultimately had to move this widget to a completely new activity. However this did work flawlessly. But if anyone finds a better answer, please do share.
As Romain Guy (Google Engineer works on UI toolkit) Said in his post
By setting the width to wrap_content you are telling ListView to be as wide as the widest of its children. ListView must therefore measure its items and to get the items it has to call getView() on the Adapter. This may happen several times depending on the number of layout passes, the behavior of the parent layout, etc.
So if you set the layout width or layout height of your ListView to wrap_content the ListView will try to measure every single view that is attached to it - which is definitely not what you want.
Keep in mind: avoid setting wrap_content for ListViews or GridViews or ExpandableListView at all times, for more details see this Google I/O video talking about the world of listview
So Make ExpandableListView to match_parent
I know the question was already answered but I have another solution which could work for more cases: I set the initial height of the list view by counting the number of items, and then update this height on collapse/expand of group by retrieving children count of the expanded/collapsed group. Here is a sample code:
int item_size = 50; // 50px
int sub_item_size = 40; // 40px
expListView.getLayoutParams().height = item_size*myAdapter.getGroupCount();
expenseListView.setAdapter(myAdapter);
// ListView Group Expand Listener
expenseListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int groupPosition) {
int nb_children = myAdapter.getChildrenCount(groupPosition);
expenseListView.getLayoutParams().height += sub_item_size*nb_children;
}
});
// Listview Group collapsed listener
expenseListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
#Override
public void onGroupCollapse(int groupPosition) {
int nb_children = myAdapter.getChildrenCount(groupPosition);
expenseListView.getLayoutParams().height -= sub_item_size*nb_children;
}
});
Normally we can't use ListView, GridView or ExpandableListView inside Scrollview. If you need it, then you have to give fixed height to your expandable ListView.
private void setListViewHeight(final ExpandableListView listView, final int groupPosition) {
final ExpandableListAdapter listAdapter = (ExpandableListAdapter) listView.getExpandableListAdapter();
int totalHeight = 0;
// be careful with unspecified width measure spec, it will ignore all layout params and even
// screen dimensions, so to get correct height, first get maximum width View can use and
// call measure() this way
final int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.EXACTLY);
for (int i = 0; i < listAdapter.getGroupCount(); i++) {
final View groupItem = listAdapter.getGroupView(i, false, null, listView);
groupItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
totalHeight += groupItem.getMeasuredHeight();
// count only expanded other groups or the clicked collapsed one
if (((listView.isGroupExpanded(i)) && (i != groupPosition))
|| ((!listView.isGroupExpanded(i)) && (i == groupPosition))) {
for (int j = 0, childrenNumber = listAdapter.getChildrenCount(i); j < childrenNumber; j++) {
final View listItem = listAdapter.getChildView(i, j, j == childrenNumber - 1, null, listView);
listItem.measure(desiredWidth, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
totalHeight += listItem.getMeasuredHeight();
}
}
}
final ViewGroup.LayoutParams params = listView.getLayoutParams();
final int height = totalHeight
+ (listView.getDividerHeight() * (listAdapter.getGroupCount() - 1));
params.height = height;
listView.setLayoutParams(params);
listView.requestLayout();
}
and assign this method on list group click:
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
#Override
public boolean onGroupClick(final ExpandableListView expandableListView, final View view, final int groupPosition, final long id) {
setListViewHeight(expandableListView, groupPosition);
return false;
}
});
IMPORTANT: although this code should work in most cases, I had some problems when child list items were TextViews with android:layout_height="wrap_content", hence their height depended on their width, which might be connected to this problem: ViewGroup{TextView,...}.getMeasuredHeight gives wrong value is smaller than real height. Please, write a comment if you manage to fix it.
Unfortunately, there doesn't seem to be a way to use wrap_content as the height of the ExpandableListView. Instead, setting a fixed height and android:nestedScrollingEnabled="true" worked for me.
I found a way better solution to achieve this phenomena with the following less complex and much efficient Kotlin code.
before all keep some points in mind :
Used fixed height Parent and Child views.
Extracted the heights of both Parent and Child into dimens.xml file.
Set divider='#null', scrollbars="none", groupIndicator="#null" (for customGroupIndicator, otherwise ignore) in ExpandableListView.
Never use paddingVertical including paddingTop and paddingBottom, replace them with layoutMargins for views having dynamic height.
Code Logic (For me this chunk is a part of a ViewModel Function named initUi(uiObject))
//Here `ui` is a DataBinding object.
//You can use ViewBinding or simple Android findViewById as per your comfort.
val data: List<ModelClass> = listObject
val context = ui.root.context
ui.expandableList.apply {
val mAdapter = MyExpandableListAdapter(data, context)
setAdapter(mAdapter)
//fetching parent height from dimens.xml
val parentViewHeight = ceil(
context.resources.getDimension(R.dimen.layout_menu_parent_height)
).toInt()
//fetching child height from dimens.xml
val childViewHeight = ceil(
context.resources.getDimension(R.dimen.layout_menu_child_height)
).toInt()
//Setting up the initial height of view when All parents are collapsed
layoutParams.height = data.size * parentViewHeight
setOnGroupExpandListener { groupPosition ->
//Increasing the height for Expanded SubCategory
layoutParams.height += mAdapter.getChildrenCount(groupPosition) * childViewHeight
}
setOnGroupCollapseListener { groupPosition ->
//Decreasing the height for Collapsed SubCategory
layoutParams.height -= mAdapter.getChildrenCount(groupPosition) * childViewHeight
}
}
MyExpandableListAdapter.kt
class FragmentMenuExpandableListAdapter(
val data: List<Data>, val context: Context
): BaseExpandableListAdapter() {
//Declaring inflater Once can save a lot of CPU and RAM uses...
private val inflater = LayoutInflater.from(context)
override fun getGroupCount(): Int = data.size
override fun getChildrenCount(groupPosition: Int): Int = data[groupPosition].subcategories.size
override fun getGroup(groupPosition: Int): Data = data[groupPosition]
override fun getChild(groupPosition: Int, childPosition: Int) = data[groupPosition].subcategories[childPosition]
override fun getGroupId(groupPosition: Int): Long = groupPosition.toLong()
override fun getChildId(groupPosition: Int, childPosition: Int): Long = "$groupPosition$childPosition0".toLong()
override fun hasStableIds(): Boolean = true
override fun getGroupView(
groupPosition: Int,
isExpanded: Boolean,
convertView: View?,
parent: ViewGroup
): View {
val binding = LayoutMenuParentBinding.inflate(inflater, parent, false)
binding.apply {
val temp = getGroup(groupPosition)
title.text = temp.category_name
//here I used a custom ImageView as GroupIndicator
//So following code chunk is Optional
if(temp.subcategories.isEmpty()) dropdownIcon.visibility = View.GONE
else dropdownIcon.let { icon ->
val view = parent as ExpandableListView
icon.setImageResource(
if(view.isGroupExpanded(groupPosition)) {
icon.setOnClickListener { view.collapseGroup(groupPosition) }
R.drawable.ic_state_expanded
}
else {
icon.setOnClickListener {
view.expandGroup(groupPosition)
}
R.drawable.ic_state_collapsed
}
)
}
root.setOnClickListener {
TODO("Do on Parent click")
}
}
return binding.root
}
override fun getChildView(
groupPosition: Int,
childPosition: Int,
isLastChild: Boolean,
convertView: View?,
parent: ViewGroup?
): View {
val binding = LayoutMenuChildBinding.inflate(
inflater, parent, false
).apply {
val temp = getChild(groupPosition, childPosition)
title.text = temp.categoryName
root.setOnClickListener {
TODO("Do on Child click")
}
}
return binding.root
}
override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean = true
}

Setting viewpager height to content's height

I have an expandablelistview which contains multiple viewpagers, and each viewpager contains multiple pages which each have a linearlayout of data. The data in the linearlayout is dynamic (there could be 5 rows or there could be 20), and each row has a dynamic height (it could have 1 line of text or 2-3).
As the viewpager requires a hard-coded height to be set for it to display, I need to display all of the content in each page's linearlayout, and I can't allow the content within the viewpager to scroll, I'm in the difficult situation of trying to calculate the height of the viewpager's active page view and then dynamically update the height of the viewpager so that the whole linearlayout content is visible. As a new page is swiped to, it should then update the viewpager height accordingly.
I'm attempting to do this within the pager adapter's SetPrimaryItem method, however the height that is coming back is close but not accurate. In my pager page view I have an outer relativelayout with a blue background, and then an inner one with a red background. For some reason the outer (blue) layout is going maybe 70dp further than the bottom of the data, I'm guessing the pager is manipulating its height behind-the-scenes. The inner one (red) behaves correctly and wraps the data within it, so this is the one I'm trying to calculate the height of so I can update the viewpager's height, however its height is incorrectly being calculated as bigger than it is, so after I update the viewpager height there is empty space at the bottom. See the screenshot, as it probably makes more sense than what I just typed:
Another acceptable solution would be to calculate the longest page's height and set the viewpager to that once, and then don't update it as the page changes, but I haven't gone that route yet and would still run into the same issue calculating the correct height.
I've spent a day on this already, any ideas / suggestions for how I can accurately update the viewpager height to fit its contents would be appreciated. I've tried a lot of other solutions on SO and haven't found one that actually worked. Here is where I've gotten so far with it:
Pager view:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v4.view.ViewPager
android:id="#+id/property_details_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
Pager adapter: (SetPrimaryItem method is where I try to update the pager's height dynamically)
public class PropertyDetailsPagerAdapter : PagerAdapter {
private ViewPager pager;
private List<List<KeyValuePair<string, string>>> lists;
public PropertyDetailsPagerAdapter (View view, List<List<KeyValuePair<string, string>>> lists) {
this.pager = view.FindViewById<ViewPager> (Resource.Id.property_details_pager);
this.pager.OffscreenPageLimit = lists.Count;
this.lists = lists;
}
public void Update (List<List<KeyValuePair<string, string>>> lists) {
this.pager.Adapter = null;
this.lists = lists;
this.pager.Adapter = this;
this.pager.Adapter.NotifyDataSetChanged ();
}
#region Setup Views
public override Java.Lang.Object InstantiateItem (View collection, int position) {
var layoutInflater = (LayoutInflater)ApplicationContext.Activity.GetSystemService (Context.LayoutInflaterService);
var view = layoutInflater.Inflate (Resource.Layout.PropertyDetailsPagerItem, null);
var pagerList = view.FindViewById<LinearLayout> (Resource.Id.property_details_pager_list);
var list = this.lists [position];
new PropertyDetailsPagerListAdapter (pagerList, Resource.Layout.PropertyDetailsPagerKeyValueRow, list);
var listViewSwipe = view.FindViewById<LinearLayout> (Resource.Id.property_details_pager_swipe);
listViewSwipe.Visibility = this.lists.Count > 1 ? ViewStates.Visible : ViewStates.Gone;
this.pager.AddView(view);
return view;
}
public override void SetPrimaryItem (View container, int position, Java.Lang.Object #object) {
base.SetPrimaryItem (container, position, #object);
var view = this.pager.GetChildAt (position);
var innerView = view.FindViewById<RelativeLayout> (Resource.Id.property_details_pager_item);
innerView.Measure (0, 0);
var parameters = this.pager.LayoutParameters as RelativeLayout.LayoutParams;
parameters.Height = innerView.MeasuredHeight;// TODO: Why isn't this height accurate?!?!
this.pager.LayoutParameters = parameters;
}
#endregion
#region Infrastructure
// Other irrelevant methods
public override int Count {
get {
return this.lists.Count;
}
}
#endregion
public List<List<KeyValuePair<string, string>>> Lists { get { return this.lists; } }
}
View for each page within the pager:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/blue">
<RelativeLayout
android:id="#+id/property_details_pager_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/red">
<LinearLayout
android:id="#+id/property_details_pager_swipe"
android:layout_below="#+id/dotted"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:visibility="invisible" />
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:background="#color/light_gray">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Swipe for\nmore records"
android:textColor="#color/gray"
android:gravity="right"
android:padding="#dimen/padding" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="#+id/property_details_pager_list"
android:layout_below="#+id/property_details_pager_swipe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</RelativeLayout>
</RelativeLayout>
Adapter for each page within the pager:
public class PropertyDetailsPagerListAdapter
{
private int resourceId;
private List<KeyValuePair<string, string>> keyValuePairs;
public PropertyDetailsPagerListAdapter (LinearLayout pagerList, int resourceId, List<KeyValuePair<string, string>> keyValuePairs) {
this.resourceId = resourceId;
this.keyValuePairs = keyValuePairs;
pagerList.RemoveAllViews ();
foreach (var keyValuePair in this.keyValuePairs) {
pagerList.AddView (GetView (keyValuePair));
}
}
public View GetView (KeyValuePair<string, string> keyValuePair) {
var layoutInflater = (LayoutInflater)ApplicationContext.Activity.GetSystemService (Context.LayoutInflaterService);
var view = layoutInflater.Inflate(resourceId, null);
// Assumes Resource.Layout.PropertyDetailsPagerKeyValueRow
view.FindViewById<TextView> (Android.Resource.Id.Text1).Text = keyValuePair.Key;
view.FindViewById<TextView> (Android.Resource.Id.Text2).Text = keyValuePair.Value;
return view;
}
}
I was able to figure it out. The issue was that I was using innerView.Measure (0,0) and then getting the MeasuredHeight, however that was giving a bogus height. After calling Measure correctly, which entails passing a real Width into it, the correct Height is returned!!
public override void SetPrimaryItem (View container, int position, Java.Lang.Object #object) {
base.SetPrimaryItem (container, position, #object);
var view = this.pager.GetChildAt (position);
var innerView = view.FindViewById<RelativeLayout> (Resource.Id.property_details_pager_item);
var desiredWidth = MeasureSpec.MakeMeasureSpec (this.pager.Width, MeasureSpecMode.AtMost);
innerView.LayoutParameters = new RelativeLayout.LayoutParams (RelativeLayout.LayoutParams.WrapContent, RelativeLayout.LayoutParams.WrapContent);
innerView.Measure (desiredWidth, (int)MeasureSpecMode.Unspecified);
var parameters = this.pager.LayoutParameters as RelativeLayout.LayoutParams;
parameters.Height = innerView.MeasuredHeight;
this.pager.LayoutParameters = parameters;
}

Categories

Resources