6

Within my current Android application, I have a screen that displays an android.support.v4.app.DialogFragment.

This DialogFragment view contains the following UI components

HEADING
== Sub Heading
== NestedScrollView
==== RecyclerView
==== RadioGroup
==== Spinner
==== EditText
==== Action Buttons

The DialogFragment is configured to be Full Screen using Style as follows:-

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setStyle(DialogFragment.STYLE_NO_TITLE, R.style.AppDialogTheme);
}

My dialog style is

<!-- Define your custom dialog theme here extending from base -->
<style name="AppDialogTheme" parent="Theme.AppCompat.Light.Dialog">
    <!-- Define color properties as desired -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">#000</item>
    <item name="android:textColorHighlight">@color/background_url</item>
    <item name="colorAccent">@color/dark_grey</item>
    <item name="colorControlNormal">@color/colorPrimaryDark</item>
    <!-- Define window properties as desired -->
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowBackground">@android:color/white</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:windowCloseOnTouchOutside">false</item>
</style>

The reason I employ a NestedScrollView is so that the View will work in both Portrait and Landscape mode.

I wish to limit the height of the RecyclerView

The closest I have got is using the layout below.

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/headline_literal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:padding="10dp"
        android:text="Heading"
        android:textSize="20sp"
        android:textStyle="bold" />

    <View
        android:id="@+id/divider"
        android:layout_width="fill_parent"
        android:layout_height="2dp"
        android:layout_marginTop="5dp"
        android:background="#c0c0c0" />

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        tools:context=".MainActivity">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:weightSum="5"
            android:orientation="vertical">

            <TextView
                android:id="@+id/sub_headline_literal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:padding="10dp"
                android:text="Some long texts having a long size so that it takes multiple lines in the view to replicate the real-life app use case. This is important to have 3-4 lines this textview so that we can see if the views are being populated correctly. Hope this sentence is long enough to replicate the real-life scenario of this TextView content. Thank you."
                android:textSize="16sp"
                android:textStyle="normal" />

            <android.support.v7.widget.RecyclerView
                android:id="@+id/dummy_rv"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_margin="10dp"
                android:layout_marginStart="9dp"
                android:layout_marginEnd="9dp"
                android:layout_weight="1"
                android:background="@drawable/rv_border"
                android:fadingEdge="horizontal"
                android:fadingEdgeLength="10dp"
                android:padding="10dp"
                android:requiresFadingEdge="vertical" />

            <RadioGroup
                android:id="@+id/myRadioGroup"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:checkedButton="@+id/sound">

                <RadioButton
                    android:id="@+id/sound"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Sound" />

                <RadioButton
                    android:id="@+id/vibration"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Vibration" />

                <RadioButton
                    android:id="@+id/silent"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Silent" />
            </RadioGroup>

            <EditText
                android:id="@+id/notes"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Notes" />

            <LinearLayout
                android:id="@+id/buttons"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:padding="10dp">

                <TextView
                    android:id="@+id/cancel_button"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:text="Cancel" />

                <TextView
                    android:id="@+id/submit_button"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:text="Submit" />
            </LinearLayout>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
</LinearLayout>

By using weightSum on the inner LinearLayout of the NestedScrollView I can limit the height of the Recyclerview. However the NestedScrollView height is far too large, with more than half its height being blank.

How can I limit the height of my RecyclerView and get NestedScrollView to wrap_content?

I've tried NestedScrollView with height wrap_content but this has no effect.

How can I achieve the desired UI? Thanks in advance!

Reaz Murshed
  • 21,071
  • 12
  • 69
  • 87
Hector
  • 2,024
  • 15
  • 69
  • 142
  • 1
    Can you please check the answer below? – Reaz Murshed Feb 25 '19 at 19:13
  • 1
    Why not use wrap_content on the recycler view instead of giving it a weight? – ahomphophone Feb 25 '19 at 21:10
  • I've tried that and it doesn't work – Hector Feb 25 '19 at 21:11
  • 1
    Did you try the updated answer? Let me know if that solves your problem. :) – Reaz Murshed Feb 27 '19 at 07:17
  • This is close to my requirements however theres one detail I would want to control the maximum height of the recyclerView while still allowing it to "shrink" smaller that the fixed max height when there are "less" items to display... Say only allowing the recyclerView height to increase upto a max of 5 visible items. Once there are greater than 5 the recyclerView would remain at this fixed height and the user will scroll through the remainder of the items. – Hector Feb 27 '19 at 08:22
  • check the below answer. It will help you for shrink the recyclerview if items height is less than maxheight and scrollable when item heights reach or cross the max height. – Jitesh Prajapati Feb 27 '19 at 10:56
  • 1
    Dude just use constraint layout. there are property for max height. – Harin Kaklotar Mar 01 '19 at 09:42
  • @HarinKaklotar, I've tried constraint layout and it doesnt work inside a NestScollView – Hector Mar 01 '19 at 09:46

4 Answers4

3

Instead of having a NestedRecyclerView, I would like to suggest to have a header and a footer added to your RecyclerView which will nicely place the overall content as far as I have seen your layout. I want to provide you a link to my answer here where you can find how to add a footer and a header along with your RecyclerView.

Hence, I would like to suggest to create a view with headline_literal and the divider and use this as a header whereas the RadioGroup, EditText and the Button will be in the footer. Let me know if you face any problem with it.

I have tried to implement the behavior that you want by myself and let me know if the following implementation works for you. I have added this in Github as well.

Let us first declare an adapter for adding a header and a footer to the RecyclerView.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

public class RecyclerViewWithHeaderFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int FOOTER_VIEW = 1;
    private static final int HEADER_VIEW = 2;
    private ArrayList<String> data; // Take any list that matches your requirement.
    private Context context;

    // Define a constructor
    public RecyclerViewWithHeaderFooterAdapter(Context context, ArrayList<String> data) {
        this.context = context;
        this.data = data;
    }

    // Define a ViewHolder for Header view
    public class HeaderViewHolder extends ViewHolder {
        public HeaderViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the item
                }
            });
        }
    }

    // Define a ViewHolder for Footer view
    public class FooterViewHolder extends ViewHolder {
        public FooterViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the item
                }
            });
        }
    }

    // Now define the ViewHolder for Normal list item
    public class NormalViewHolder extends ViewHolder {
        public NormalViewHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the normal items
                }
            });
        }
    }

    // And now in onCreateViewHolder, you have to pass the correct view
    // while populating the list item.

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View v;

        if (viewType == FOOTER_VIEW) {
            v = LayoutInflater.from(context).inflate(R.layout.list_item_footer, parent, false);
            FooterViewHolder vh = new FooterViewHolder(v);
            return vh;
        } else if (viewType == HEADER_VIEW) {
            v = LayoutInflater.from(context).inflate(R.layout.list_item_header, parent, false);
            HeaderViewHolder vh = new HeaderViewHolder(v);
            return vh;
        }

        // Otherwise populate normal views
        v = LayoutInflater.from(context).inflate(R.layout.list_item_normal, parent, false);
        NormalViewHolder vh = new NormalViewHolder(v);

        return vh;
    }

    // Now bind the ViewHolder in onBindViewHolder
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        try {
            if (holder instanceof NormalViewHolder) {
                NormalViewHolder vh = (NormalViewHolder) holder;

                vh.bindView(position);
            } else if (holder instanceof FooterViewHolder) {
                FooterViewHolder vh = (FooterViewHolder) holder;
            } else if (holder instanceof HeaderViewHolder) {
                HeaderViewHolder vh = (HeaderViewHolder) holder;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Now the critical part. You have return the exact item count of your list
    // I've only one footer. So I returned data.size() + 1
    // If you've multiple headers and footers, you've to return total count
    // like, headers.size() + data.size() + footers.size()

    @Override
    public int getItemCount() {
        if (data == null) {
            return 0;
        }

        if (data.size() == 0) {
            // Return 1 here to show nothing
            return 1;
        }

        // Add extra view to show the header view
        // Add another extra view to show the footer view
        // So there are two extra views need to be populated
        return data.size() + 2;
    }

    // Now define getItemViewType of your own.
    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            // This is where we'll add the header.
            return HEADER_VIEW;
        } else if (position == data.size() + 1) {
            // This is where we'll add a footer.
            return FOOTER_VIEW;
        }

        return super.getItemViewType(position);
    }

    // So you're done with adding a footer and its action on onClick.
    // Now set the default ViewHolder for NormalViewHolder
    public class ViewHolder extends RecyclerView.ViewHolder {
        // Define elements of a row here
        public ViewHolder(View itemView) {
            super(itemView);
            // Find view by ID and initialize here
        }

        public void bindView(int position) {
            // bindView() method to implement actions
        }
    }
}

Now let us define the layouts one by one. Here is the list_item_normal.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/normal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="This is a text to be displayed in each item in the RecyclerView"
        android:textSize="16sp"
        android:textStyle="normal" />

</LinearLayout>

And the list_item_footer.xml should look like the following.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:orientation="vertical">

    <RadioGroup
        android:id="@+id/myRadioGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:checkedButton="@+id/sound">

        <RadioButton
            android:id="@+id/sound"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Sound" />

        <RadioButton
            android:id="@+id/vibration"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Vibration" />

        <RadioButton
            android:id="@+id/silent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Silent" />

    </RadioGroup>

    <EditText
        android:id="@+id/notes"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Notes" />

    <LinearLayout
        android:id="@+id/buttons"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp">

        <TextView
            android:id="@+id/cancel_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="Cancel" />

        <TextView
            android:id="@+id/submit_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="Submit" />
    </LinearLayout>
</LinearLayout>

Finally, the list_item_header.xml should have the following.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/sub_headline_literal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="10dp"
        android:text="Some long texts having a long size so that it takes multiple lines in the view to replicate the real-life app use case. This is important to have 3-4 lines this textview so that we can see if the views are being populated correctly. Hope this sentence is long enough to replicate the real-life scenario of this TextView content. Thank you."
        android:textSize="16sp"
        android:textStyle="normal" />

</LinearLayout>

Now you have divided the components of your original layout into parts. Hence the main layout should look like the following.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/headline_literal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:padding="10dp"
        android:text="Heading"
        android:textSize="20sp"
        android:textStyle="bold" />

    <View
        android:id="@+id/divider"
        android:layout_width="fill_parent"
        android:layout_height="2dp"
        android:layout_marginTop="5dp"
        android:background="#c0c0c0" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/dummy_rv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:padding="10dp" />
</LinearLayout>

Hence, I am sharing one sample Activity to run this code which will show the overall implementation.

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private ArrayList<String> data = new ArrayList<String>();
    private RecyclerViewWithHeaderFooterAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initializeData();
        initializeRecyclerView();
    }

    private void initializeRecyclerView() {
        mRecyclerView = findViewById(R.id.dummy_rv);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        adapter = new RecyclerViewWithHeaderFooterAdapter(this, data);
        mRecyclerView.setAdapter(adapter);
    }

    private void initializeData() {
        for (int i = 0; i < 10; i++) data.add("Position :" + i);
    }
}

Hope that helps!

Reaz Murshed
  • 21,071
  • 12
  • 69
  • 87
  • Thanks for taking the time to look at my question. I didn't want to have to fix the height of the recyclerView as sometimes I only have one or two items to display, and with a fixed height the UI looks bad. I really desire a solution that actually wars_content correctly of the recyclerView – Hector Feb 25 '19 at 20:49
  • Oh I see. Let me update the answer then. I think I know what you are looking for. – Reaz Murshed Feb 25 '19 at 20:54
  • Please check the updated answer. I have linked to one of my answer where you will find how to add a header/footer in a `RecyclerView`. Hope that helps to solve your problem. – Reaz Murshed Feb 25 '19 at 21:00
  • Hey, I have updated the answer again with the code for adding the header and a footer in your case. Hope that will help you. Good luck! – Reaz Murshed Feb 25 '19 at 21:44
1

Customize Recycler view to set maxHeight.

public class MaxHeightRecyclerView extends RecyclerView {
    private int mMaxHeight;

    public MaxHeightRecyclerView(Context context) {
        super(context);
    }

    public MaxHeightRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize(context, attrs);
    }

    public MaxHeightRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize(context, attrs);
    }

    private void initialize(Context context, AttributeSet attrs) {
        TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView);
        mMaxHeight = arr.getLayoutDimension(R.styleable.MaxHeightScrollView_maxHeight, mMaxHeight);
        arr.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mMaxHeight > 0) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

n attrs.xml

<declare-styleable name="MaxHeightScrollView">
        <attr name="maxHeight" format="dimension" />
    </declare-styleable>

set RecyclerView height wrap_content in xml and maxHeight to fixwidth in dp. The RecyclerView will consume height wrap_content till fixWidth which you set, after reaching to maxHeight, the RecyclerView will scrollable.

Jitesh Prajapati
  • 2,605
  • 4
  • 25
  • 45
0

If you also need to limit the size of your NestedScrollView by makeing a custom NestedScrollView:

public class CustomNestedScrollView extends NestedScrollView {

private int maxHeight;
private final int defaultHeight = 200;

public CustomNestedScrollView(Context context) {
    super(context);
}

public CustomNestedScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    if (!isInEditMode()) {
        init(context, attrs);
    }
}

public CustomNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    if (!isInEditMode()) {
        init(context, attrs);
    }
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CustomNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    if (!isInEditMode()) {
        init(context, attrs);
    }
}

private void init(Context context, AttributeSet attrs) {
    if (attrs != null) {
        TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.CustomNestedScrollView);
        //200 is a defualt value
        maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.CustomNestedScrollView_maxHeight, defaultHeight);

        styledAttrs.recycle();
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

attr.xml

<declare-styleable name="CustomNestedScrollView">
        <attr name="maxHeight" format="dimension" />
    </declare-styleable>

example layout for custom NestedScrollView:

               <your.package.CustomNestedScrollView 
                android:layout_weight="1"
                app:maxHeight="90dp"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">

                <!--Child view with RecyclerView here-->


                </your.package.CustomNestedScrollView>

Along with this custom NestedScrollView if you apply the customization of your RecyclerView then it will work exactly how you want. I hope this helps!

Imtiaz Abir
  • 591
  • 3
  • 12
-1

Easy,

Use maxHeight attribute with specific height, and if it still doesn't work, then try creating a customrecyclerview extending RecyclerView and override height in OnMeasure method.

Sandip Savaliya
  • 718
  • 6
  • 19