Horizontal RecyclerView with dynamic item’s Height
Asked Answered
D

6

14

I’m trying to implement a RecyclerView with horizontal scrolling, so I’m using this a LinearLayoutManager with horizontal orientation. The problem is that I’m populating the RecyclerView using 2 different types of items, with different heights. This is the layout I’m using for the item:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp">

<LinearLayout
    android:id="@+id/document_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="@drawable/ic_rounded"
    android:backgroundTint="@color/ms_black_ms_gray"
    android:gravity="center"
    android:layout_gravity="bottom"
    android:padding="5dp"
    android:paddingStart="15dp">

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:textSize="13sp"
        android:singleLine="true"
        android:maxWidth="80dp"
        tools:text="example_form"/>

    <TextView
        android:id="@+id/format"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:textSize="13sp" />

    …
</LinearLayout>

<android.support.v7.widget.CardView
    android:id="@+id/image_view"
    android:layout_width="120dp"
    android:layout_height="80dp"
    android:layout_gravity="bottom"
    app:cardCornerRadius="25dp"
    app:cardElevation="0dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/preview_image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY"/>

        …

    </RelativeLayout>
</android.support.v7.widget.CardView>

and this is the layout that contains the RecyclerView, which is basically like this:

enter image description here

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingStart="14dp"
        android:paddingEnd="14dp">

        <ImageView
            android:id="@+id/attach"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:layout_gravity="bottom"
            android:layout_marginBottom="19dp"
            android:visibility="visible"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:layout_marginBottom="10dp"
            android:layout_marginTop="5dp"
            android:padding="3dp"
            android:foreground="@drawable/ic_rounded_stroke"
            android:foregroundTint="@color/white">
            <android.support.constraint.ConstraintLayout
                android:id="@+id/chatEdit"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/ic_rounded"
                android:foreground="@drawable/ic_rounded_stroke"
                android:padding="6dp"
                android:visibility="visible">

                <EditText
                    android:id="@+id/editText"
                    android:textSize="17sp"
                    android:textColor="#121212"
                    android:letterSpacing="-0.02"
                    android:lineSpacingExtra="0sp"
                    android:padding="10dp"
                    android:paddingStart="15dp"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:maxLines="5"
                    android:hint="@string/chat_hint"
                    android:inputType="textCapSentences|textMultiLine"
                    android:maxLength="2500"
                    android:background="@null"
                    app:layout_constraintRight_toLeftOf="@id/buttonsContainer"
                    app:layout_constraintLeft_toLeftOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />


                    <TextView
                        android:id="@+id/send"
                        android:layout_gravity="bottom"
                        android:visibility="visible"
                        android:paddingLeft="15dp"
                        android:paddingRight="15dp"
                        android:paddingBottom="10dp"
                        android:paddingTop="8dp"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textSize="18sp"
                        android:textColor="#ffffff"
                        android:letterSpacing="-0.02"
                        android:gravity="center_horizontal"
                        android:text="@string/send"
                        app:layout_constraintRight_toRightOf="parent"
                        app:layout_constraintBottom_toBottomOf="parent"
                        />

                <android.support.v7.widget.RecyclerView
                    android:id="@+id/filesList"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_marginTop="10dp"
                    android:paddingTop="5dp"
                    android:paddingEnd="5dp"
                    android:visibility="gone"
                    app:layout_constraintRight_toLeftOf="@id/send"
                    app:layout_constraintLeft_toLeftOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/editText"
                    app:layout_constraintBottom_toBottomOf="parent"/>
            </android.support.constraint.ConstraintLayout>
        </LinearLayout>
    </LinearLayout>

I’m using a single ViewHolder, I just change the visibility of the 2 child views.

The result I expect to get is this one:

enter image description here

But what I’m getting is this; the CardView being cut in half, using the height of the second type of item:

enter image description here

I saw this post, which is similar to my problem. It recommends using Google’s Flexbox. So, I tried to implement FlexboxLayoutManager:

FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(getContext());
layoutManager.setFlexDirection(FlexDirection.ROW);
layoutManager.setFlexWrap(FlexWrap.NOWRAP);

I’m using row direction and It is showing items on next lines if it does not fit in single line. So, I also added No_wrap. And now it is showing items in a single line but do not provide scrolling. Also in this case it tries to fit all items in a single line by decreasing width of items.

I also played with the flex box sample app, but I couldn’t get the result I want.

Is there a way I can achieve horizontal scrolling with the Flexbox integrated with RecyclerView? Or should I use a different approach?

Thanks

EDIT

Thanks for the tips and everything, but it is not solving it. So, I stripped down the code to bare minimum to reproduce this.

MainActivity:

public class MainActivity extends AppCompatActivity {

private static final int REQUEST_CODE = 1;

private RecyclerView recyclerView;
private FilesAdapter filesAdapter;
private List<File> filesList = new ArrayList<>();

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

    recyclerView = findViewById(R.id.recyclerView);
    LinearLayoutManager filesLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
    recyclerView.setLayoutManager(filesLayoutManager);
    filesAdapter = new FilesAdapter(filesList);
    recyclerView.setAdapter(filesAdapter);

    ImageView attach = findViewById(R.id.attach);
    attach.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent = new Intent();
            intent.setType("*/*");
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setAction(Intent.ACTION_GET_CONTENT);
            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
            startActivityForResult(Intent.createChooser(intent,"Select Files"), REQUEST_CODE);
        }
    });
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
        try {
            if (data != null) {
                List<File> uriList = new ArrayList<>();
                if (data.getClipData() != null) { // Multiple files
                    for (int i = 0; i < data.getClipData().getItemCount(); i++) {
                        Uri uri = data.getClipData().getItemAt(i).getUri();
                        Pair<Boolean, File> isValid = isFileValid(uri);
                        if (isValid.first) {
                            uriList.add(isValid.second);
                        }
                    }
                } else { // Single file
                    Uri uri = data.getData();
                    Pair<Boolean, File> isValid = isFileValid(uri);
                    if (isValid.first) {
                        uriList.add(isValid.second);
                    }
                }

                if (uriList.size() > 0) {
                    for (File file : uriList) {
                        filesList.add(filesList.size(), file);
                        filesAdapter.notifyItemInserted(filesList.size());
                    }
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    super.onActivityResult(requestCode, resultCode, data);
}

private Pair<Boolean, File> isFileValid(Uri uri) throws NullPointerException {
    Pair<Boolean, File> defaultResponse = Pair.create(false, null);
    Cursor c = getContentResolver().query(uri, null, null, null, null);
    if (c != null) {
        c.moveToFirst();

        String filename = c.getString(c.getColumnIndex(OpenableColumns.DISPLAY_NAME));

        if (isSupported(filename)) {
            c.close();
            return Pair.create(true, new File(StringUtils.endsWithIgnoreCase(filename, ".pdf") ? DOCUMENT : IMAGE));
        } else {
            Toast.makeText(this, "File format not supported", Toast.LENGTH_SHORT).show();
            c.close();
            return defaultResponse;
        }
    }
    return defaultResponse;
}

private boolean isSupported(String filename) {
    String[] supportedFormats = { ".pdf", ".jpg", ".gif", ".png" };

    for (String format : supportedFormats) {
        if (StringUtils.endsWithIgnoreCase(filename, format)) {
            return true;
        }
    }
    return false;
}
}

Main activity layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:gravity="bottom"
android:orientation="vertical"
tools:context=".MainActivity">

   <androidx.recyclerview.widget.RecyclerView
   android:id="@+id/recyclerView"
   android:layout_width="match_parent"
   android:layout_height="wrap_content" />

<ImageView
    android:id="@+id/attach"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|center"
    android:layout_marginBottom="19dp"
    android:padding="10dp"
    android:src="@drawable/ic_attach" />
</LinearLayout>

File:

public class File {

public enum Type {
    DOCUMENT,
    IMAGE
}

private Type type;

public File(Type type) {
    this.type = type;
}

public Type getType() {
    return type;
}
}

File Adapter:

public class FilesAdapter extends RecyclerView.Adapter<FilesAdapter.BaseViewHolder> {

private List<File> files;

public FilesAdapter(List<File> files) {
    this.files = files;
}

@NonNull
@Override
public FilesAdapter.BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext()).inflate(viewType == 0 ? R.layout.document_item : R.layout.image_item, parent, false);
    if (viewType == 0) {
        return new DocumentViewHolder(view);
    } else {
        return new ImageViewHolder(view);
    }
}

@Override
public void onBindViewHolder(@NonNull FilesAdapter.BaseViewHolder viewHolder, int position) {
    viewHolder.bind(files.get(position));
}

@Override
public int getItemViewType(int position) {
    if (files.get(position).getType() == File.Type.DOCUMENT) {
        return 0;
    } else {
        return 1;
    }
}

@Override
public int getItemCount() {
    return files.size();
}

abstract static class BaseViewHolder extends RecyclerView.ViewHolder {
    public BaseViewHolder(@NonNull View itemView) {
        super(itemView);
    }
    abstract void bind(File file);
}

static class ImageViewHolder extends BaseViewHolder {

    public ImageViewHolder(@NonNull View itemView) {
        super(itemView);
    }

    @Override
    void bind(File file) { }
}

static class DocumentViewHolder extends BaseViewHolder {

    public DocumentViewHolder(@NonNull View itemView) {
        super(itemView);
    }

    public void bind(File file) { }
}
}

document item:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="40dp"
android:background="@drawable/ic_rounded"
android:backgroundTint="#888888"
android:layout_margin="5dp">

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:gravity="center"
    android:text="PDF"
    android:textColor="@android:color/white"/>

</LinearLayout>

image item:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="120dp"
android:layout_height="80dp"
android:layout_margin="5dp"
app:cardBackgroundColor="#000000"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:gravity="center"
    android:text="IMAGE"
    android:textColor="@android:color/white"/>

</androidx.cardview.widget.CardView>

if I select an image first, and the several pdfs, it works fine:

enter image description here

But if I first select 3 pdfs, and then an image, this happens:

enter image description here

Any idea how to solve this?

Dorsal answered 23/10, 2020 at 17:22 Comment(7)
you can use GridView with the recyclerView : check this #31399492Kunkle
Why are you using a single viewholder, any specific reason? Use two viewholders for two views, that should solve the problem.Banzai
Already tried that, didn't work. Instead of cutting the upper half, it cuts the lower half. that's the only difference it makesDorsal
The problem is most probably with the layout holding your RecyclerView. can you share the complete xml file so we can see what there is outside the constraint view and also what else there is inside?Exsanguine
@MehranBehbahani I updated the question to show the full layoutDorsal
why is the RecyclerView's height set to match_parent instead of wrap_content? by choosing match_parent you are constraining it to the parent and it cannot expand to show you the full view. have you tried wrap_content? also can you share a screen shot of the activity with the dissatisfying result? I still feel like you haven't shared the whole xml layout. :)Exsanguine
I updated it again, Could you check it out pleaseDorsal
B
25

I had a similar issue in another project and i solved it by using the Google library FlexboxLayoutManager.

  1. Get the latest FlexboxLayoutManager Library (https://github.com/google/flexbox-layout) and add it into your grandle dependencies (implementation 'com.google.android:flexbox:2.0.1')
  2. In your Activity add the below lines of code:
    FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(this); 
     layoutManager.setFlexDirection(FlexDirection.ROW);
     layoutManager.setFlexWrap(FlexWrap.NOWRAP);
     recyclerView.setLayoutManager(layoutManager);
  3. To make FlexboxLayoutManager work with horizontal scroll add the below code in your adapter (FilesAdapter) in BaseViewHolder class:
    abstract static class BaseViewHolder extends RecyclerView.ViewHolder {
         public BaseViewHolder(@NonNull View itemView) {
             super(itemView);
             ViewGroup.LayoutParams lp = itemView.getLayoutParams();
             if (lp instanceof FlexboxLayoutManager.LayoutParams) {
                 FlexboxLayoutManager.LayoutParams flexboxLp = (FlexboxLayoutManager.LayoutParams) lp;
                 flexboxLp.setFlexShrink(0.0f);
                 flexboxLp.setAlignSelf(AlignItems.FLEX_START); //this will align each itemView on Top or use AlignItems.FLEX_END to align it at Bottom
             }
         }
         abstract void bind(File file);
     }
Beaman answered 2/11, 2020 at 10:0 Comment(7)
beautiful, just what I was looking for!Dorsal
elegant solutionElephantine
Honestly I can't upvote this enough!! Really elegant solution. I've tried all sorts, scroll listeners, posting runnables to calculate height. But this works straight away out of the box.Stahl
The solution is working. But all the list items disappear when I try to remove an item (no issues if I remove the first or last item). Anyone else experienced the same issue?Alo
This solution is good but you should remove this line flexboxLp.setAlignSelf(AlignItems.FLEX_START); Then you will find all items in same height depending on highest one in recycler view.Declivous
Getting the same issue as you @SuneeshAmbatt . Are you able to find a solution on it?Asbury
the above solution is not wotking if itemviews have edittext. because on recycler view unnecessarily scrolls on each key pressDaedalus
S
5

In case it helps anyone else, Kotlin version of MariosP's answer with minor refactors below, but 100% kudos to @MariosP. His answer saved the day for us!

RecyclerView setup (this was from a fragment, called in onViewCreated):

private fun setupRecyclerView() {
    val flexBoxLayoutManager = FlexboxLayoutManager(requireContext(), FlexDirection.ROW, FlexWrap.NOWRAP)
    with(recycler_view) {
        layoutManager = flexBoxLayoutManager
        adapter = myAdapter
    }
}

Adapter setup:

var items : List<Item>

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    holder.bindItem(items[position])
}

In the ViewHolder:

class MyViewHolder(private val itemView: View): RecyclerView.ViewHolder(itemView) {

    fun bindItem(item: Item) {
        // Do things with item
        updateLayoutParamsToAllowHorizontalScrolling()
    }

    private fun updateLayoutParamsToAllowHorizontalScrolling() {
        (itemView.layoutParams as? FlexboxLayoutManager.LayoutParams)?.let {
            it.flexShrink = 0.0f
            it.alignSelf = AlignItems.FLEX_START
        }
    }

}
Stahl answered 29/4, 2021 at 16:52 Comment(0)
E
0

try this for your RecyclerView:

android:layout_height="wrap_content"

Since the XML file that contains your RecyclerView is not complete here I cannot be sure but if your RecyclerView is inside another parent view that is limiting it, then i guess using wrap_content as the height for RecyclerView plus some tweaks should solve it.

Also, note that you are limiting you RecyclerView to the bottom of "editText" from top side so that may be preventing your RecyclerView from expanding too.

Exsanguine answered 27/10, 2020 at 22:18 Comment(0)
E
0

The reason Image is getting cropped when you choose pdf files first is because the height of recycleView is 40dp which is the height of pdf item. When you try to add a new item without modifying the existing ones, recycleView height remains the same i.e. 40dp. To enforce a minimum height of 80dp (which is the current height of the image layout), we can use minHeight as follows:

<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="80dp"
        tools:listitem="@layout/document_item"
        />

You can also modify your pdf item layout to align the pdfs center_vertically with image items as follows:

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

    <TextView
        android:layout_width="150dp"
        android:layout_height="40dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:background="@drawable/ic_round"
        android:backgroundTint="#888888"
        android:text="PDF"
        android:textColor="@android:color/white"/>

</LinearLayout>

Cheers :)

Euripus answered 2/11, 2020 at 18:38 Comment(0)
D
-1

First, I think your main layout is a bit overcomplicated. You could do the whole thing in a single ConstraintLayout (if you need framed background around specific items, I recommend to use pure View instances layed out using Barriers and Guidelines - see https://medium.com/better-programming/essential-components-of-constraintlayout-7f4026a1eb87)

Another addition and/or improvement would be to not use right/left constraints, rather start/end. This prepares your layout for RTL display too.

Also, I highly recommend to use separate layout files and ViewHolders for distinct items in a RecyclerView.

As others pointed out in comments, your RecyclerView is layed out using match_parent which can in turn crop your view. You may want to set this wrap_content.

In the meanwhile, you may also want to update dependencies to use Android Jetpack and ditch support libraries.

Danutadanya answered 30/10, 2020 at 16:14 Comment(1)
I updated the question, Could you check it out pleaseDorsal
R
-1

All you have to do, is to set recyclerview's height to the height of the biggest item, in your case the image item.

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="wrap_content"
android:layout_height="80dp" />
Ricotta answered 1/11, 2020 at 4:16 Comment(1)
Does not work if the heights of the items may be dynamic.Mcalpine

© 2022 - 2024 — McMap. All rights reserved.