javaandroidandroid-recyclerviewandroid-nestedscrollview

NestedScrollView with wrapped RecyclerView's not scrolling


I have two separate horizontal RecyclerView's that are embedded in a vertical LinearLayout, that itself is contained within a NestedScrollView - The top and bottom views have different dataset and eventually will have different layouts.

Here is the layout.. enter image description here

I am currently unable to get the NestedScrollView to scroll the content. Instead, both RecyclerView's are handling their own scroll behaviour independently. The desired behaviour is that scrolling the NestedScrollView scrolls both RecyclerViews (the contents of the LinearLayout), so it looks like one menu.

I have looked at many SO thread's on similar, but I cannot seem to get this working. I've tried things such as..

Here is the code..

MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends AppCompatActivity {

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        getWindow().getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
    }
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // remove title
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN);

                getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);

    setContentView(R.layout.activity_main);

    if (savedInstanceState == null) {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.content, new SyncedListsActivity())
                .commitNow();
        }
    }
}

SyncedListsActivity.java

import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.core.widget.NestedScrollView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

public class SyncedListsActivity extends Fragment {
View v;
NestedScrollView scrollView;
LinearLayout scrollContent;
RecyclerView topMenu;
TopAdapter topAdapter;
RecyclerView bottomMenu;
BottomAdapter bottomAdapter;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("SyncedListsActivity", "onCreateView");
    super.onCreate(savedInstanceState);
    if(v==null) {
        Log.d("SyncedListsActivity", "v is NULL recreating");
        v = inflater.inflate(R.layout.activity_synced_scroll, container, false);
        scrollView = v.findViewById(R.id.scrollContainer);
        scrollContent = v.findViewById(R.id.wrapper);

        topMenu = v.findViewById(R.id.topList);
        topMenu.setNestedScrollingEnabled(false);
        bottomMenu = v.findViewById(R.id.bottomList);
        bottomMenu.setNestedScrollingEnabled(false);

        setupTopMenu();
        setupBottomMenu();
    }
    return v;
}

private void setupTopMenu() {
    topMenu.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
    topAdapter = new TopAdapter(getActivity(),this);
    topMenu.setAdapter(topAdapter);
    topAdapter.notifyDataSetChanged();

    SpacesItemDecoration decoration = new SpacesItemDecoration(12);
    topMenu.addItemDecoration(decoration);
}

private void setupBottomMenu() {
    bottomMenu.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
    bottomAdapter = new BottomAdapter(getActivity(),this);
    bottomMenu.setAdapter(bottomAdapter);
    bottomAdapter.notifyDataSetChanged();

    SpacesItemDecoration decoration = new SpacesItemDecoration(12);
    bottomMenu.addItemDecoration(decoration);
}

@Override
public void onResume() {
    super.onResume();
    Log.d("SyncedListsActivity", "onResume");
    if(topMenu == null) {
        setupTopMenu();
    }

    if(bottomMenu == null) {
        setupBottomMenu();
    }
}
}

class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;

public SpacesItemDecoration(int space) {
    this.space = space;
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    outRect.left = space;
    outRect.right = space;
    outRect.top = space;

    if (parent.getChildLayoutPosition(view) == 0) {
        outRect.left = 0;
    }
}
}

DelegateTile.java

class DelegateTile {
private int pk; //primary key used by recyclerview DiffUtil to simplify adding/removing items.  Can probably use a CID value for this!
private String title;
private String color;
private int width;
private int viewType;

public DelegateTile(int pk, String title, String color, int width, int viewType) {
    this.pk = pk;
    this.title = title;
    this.color = color;
    this.width = width;
    this.viewType = viewType;
}

public int getPk() { return pk; }
public int getTileType() {
    return viewType;
}
public String getTitle() {
    return title;
}
public String getColor() {
    return color;
}
public int getSize() {
    return width;
}
}

TopAdapter.java

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;

public class TopAdapter extends RecyclerView.Adapter<TopAdapter.TopAdapterView> {
private Context context;
private Fragment parent;

private List<DelegateTile> tileDefinitions;

public TopAdapter(Context context, SyncedListsActivity parent) {
    this.context = context;
    this.parent = parent;

    //Create tiles
    tileDefinitions = new ArrayList<>();
    tileDefinitions.add(new DelegateTile(0, "Item 1", "9E0026", 800, 0));
    tileDefinitions.add(new DelegateTile(1, "Item 2", "539E3E", 400, 1));
    tileDefinitions.add(new DelegateTile(2, "Item 3", "F89B1C", 800, 0));
    tileDefinitions.add(new DelegateTile(3, "Item 4", "13ADA0", 400, 1));
    tileDefinitions.add(new DelegateTile(4, "Item 5", "00507E", 400, 1));
    tileDefinitions.add(new DelegateTile(5, "Item 6", "555555", 800, 0));
}

@Override
public TopAdapterView onCreateViewHolder(ViewGroup parent, int viewType) {
    TopAdapterView gridAdapterView = null;
    Log.d("TOP_RECYCLER", "onCreateViewHolder " + viewType + " rendering");
    switch (viewType) {
        case 0: { //Wide tile
            Log.d("TOP_RECYCLER", "onCreateViewHolder " + viewType + " TILE_TYPE_WIDE");
            View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.tile_wide, parent, false);
            gridAdapterView = new TopAdapterView(layoutView, viewType);
            break;
        }
        case 1: { //normal width tile
            Log.d("TOP_RECYCLER", "onCreateViewHolder " + viewType + " TILE_TYPE_STANDARD");
            View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.tile_normal, parent, false);
            gridAdapterView = new TopAdapterView(layoutView, viewType);
            break;
        }
    }
    return gridAdapterView;
}

@Override
public void onBindViewHolder(TopAdapterView holder, int position) {
    /*
    different styles for different cells - Slideshow, tiles, Footer
    */
    switch (tileDefinitions.get(position).getTileType()) {
        case 0: { //wide tile
            //Tiles - set tile behavior
            holder.title.setText(tileDefinitions.get(position).getTitle());
            //Dynamic colors for our tiles
            ViewGroup.LayoutParams params = holder.layout.getLayoutParams();
            params.width = tileDefinitions.get(position).getSize(); //setting width
            holder.layout.setLayoutParams(params);
            holder.layout.setBackgroundColor(Color.parseColor("#" + tileDefinitions.get(position).getColor())); //setting color
            holder.layout.setTag(position);
            //margins
            ViewGroup.MarginLayoutParams llparams = (ViewGroup.MarginLayoutParams) holder.layout.getLayoutParams();
            llparams.rightMargin = 5;
        }
        case 1: { //normal width tile
            //Tiles - set tile behavior
            holder.title.setText(tileDefinitions.get(position).getTitle());
            //Dynamic colors for our tiles
            ViewGroup.LayoutParams params = holder.layout.getLayoutParams();
            params.width = tileDefinitions.get(position).getSize(); //setting width
            holder.layout.setLayoutParams(params);
            holder.layout.setBackgroundColor(Color.parseColor("#" + tileDefinitions.get(position).getColor())); //setting color
            holder.layout.setTag(position);
            //margins
            ViewGroup.MarginLayoutParams llparams = (ViewGroup.MarginLayoutParams) holder.layout.getLayoutParams();
            llparams.rightMargin = 5;

            break;
        }
    }
}

@Override
public int getItemCount() {
    return tileDefinitions == null ? 0 : tileDefinitions.size();
}

@Override
public int getItemViewType(int position) {
    // Return type based on position
    return tileDefinitions.get(position).getTileType();
}

class TopAdapterView extends RecyclerView.ViewHolder {
    TextView title;
    LinearLayout layout;
    public TopAdapterView(View itemView, int type) {
        super(itemView);
        switch (type) {
            case 0: { //wide tile
                title = itemView.findViewById(R.id.txtTitle);
                layout = itemView.findViewById(R.id.wide_layout);
                break;
            }
            case 1: { //normal width tile
                title = itemView.findViewById(R.id.txtTitle);
                layout = itemView.findViewById(R.id.normal_layout);
                break;
            }
        }
    }
}
}

BottomAdapter.java

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;

public class BottomAdapter extends RecyclerView.Adapter<BottomAdapter.BottomAdapterView> {
private Context context;
private Fragment parent;

private List<DelegateTile> tileDefinitions;

public BottomAdapter(Context context, SyncedListsActivity parent) {
    this.context = context;
    this.parent = parent;

    //Create tiles
    tileDefinitions = new ArrayList<>();
    tileDefinitions.add(new DelegateTile(0, "Item 1", "A21E23", 400, 1)); //1
    tileDefinitions.add(new DelegateTile(1, "Item 2", "008EAA", 800, 0)); //2
    tileDefinitions.add(new DelegateTile(2, "Item 3", "A8CF5A", 400, 1)); //5
    tileDefinitions.add(new DelegateTile(3, "Item 4", "814199", 400, 1)); //6
    tileDefinitions.add(new DelegateTile(4, "Item 5", "0093B2", 800, 0)); //7
    tileDefinitions.add(new DelegateTile(5, "Item 6", "13ADA0", 400, 1)); //6
    tileDefinitions.add(new DelegateTile(6, "Item 7", "00507E", 400, 1)); //7
}

@Override
public BottomAdapterView onCreateViewHolder(ViewGroup parent, int viewType) {
    BottomAdapterView gridAdapterView = null;
    Log.d("BOTTOM_RECYCLER", "onCreateViewHolder " + viewType + " rendering");
    switch (viewType) {
        case 0: { //Wide tile
            Log.d("BOTTOM_RECYCLER", "onCreateViewHolder " + viewType + " TILE_TYPE_WIDE");
            View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.tile_wide, parent, false);
            gridAdapterView = new BottomAdapterView(layoutView, viewType);
            break;
        }
        case 1: { //normal width tile
            Log.d("BOTTOM_RECYCLER", "onCreateViewHolder " + viewType + " TILE_TYPE_STANDARD");
            View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.tile_normal, parent, false);
            gridAdapterView = new BottomAdapterView(layoutView, viewType);
            break;
        }
    }
    return gridAdapterView;
}

@Override
public void onBindViewHolder(BottomAdapterView holder, int position) {
    /*
    different styles for different cells - Slideshow, tiles, Footer
    */
    switch (tileDefinitions.get(position).getTileType()) {
        case 0: { //wide tile
            //Tiles - set tile behavior
            holder.title.setText(tileDefinitions.get(position).getTitle());
            //Dynamic colors for our tiles
            ViewGroup.LayoutParams params = holder.layout.getLayoutParams();
            params.width = tileDefinitions.get(position).getSize(); //setting width
            holder.layout.setLayoutParams(params);
            holder.layout.setBackgroundColor(Color.parseColor("#" + tileDefinitions.get(position).getColor())); //setting color
            holder.layout.setTag(position);
            //margins
            ViewGroup.MarginLayoutParams llparams = (ViewGroup.MarginLayoutParams) holder.layout.getLayoutParams();
            llparams.rightMargin = 5;
        }
        case 1: { //normal width tile
            //Tiles - set tile behavior
            holder.title.setText(tileDefinitions.get(position).getTitle());
            //Dynamic colors for our tiles
            ViewGroup.LayoutParams params = holder.layout.getLayoutParams();
            params.width = tileDefinitions.get(position).getSize(); //setting width
            holder.layout.setLayoutParams(params);
            holder.layout.setBackgroundColor(Color.parseColor("#" + tileDefinitions.get(position).getColor())); //setting color
            holder.layout.setTag(position);
            //margins
            ViewGroup.MarginLayoutParams llparams = (ViewGroup.MarginLayoutParams) holder.layout.getLayoutParams();
            llparams.rightMargin = 5;

            break;
        }
    }
}

@Override
public int getItemCount() {
    return tileDefinitions == null ? 0 : tileDefinitions.size();
}

@Override
public int getItemViewType(int position) {
    // Return type based on position
    return tileDefinitions.get(position).getTileType();
}

class BottomAdapterView extends RecyclerView.ViewHolder {
    TextView title;
    LinearLayout layout;
    public BottomAdapterView(View itemView, int type) {
        super(itemView);
        switch (type) {
            case 0: { //wide tile
                title = itemView.findViewById(R.id.txtTitle);
                layout = itemView.findViewById(R.id.wide_layout);
                break;
            }
            case 1: { //normal width tile
                title = itemView.findViewById(R.id.txtTitle);
                layout = itemView.findViewById(R.id.normal_layout);
                break;
            }
        }
    }
}
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:id="@+id/main_root"
android:layout_margin="20dp"
android:clipToPadding="false"
android:clipChildren="false">

<FrameLayout
    android:id="@+id/content"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:clipChildren="false"
    android:clipToPadding="false" />
</androidx.constraintlayout.widget.ConstraintLayout>

activity_synced_scroll.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:id="@+id/main_root"
android:clipToPadding="false"
android:clipChildren="false">

<androidx.core.widget.NestedScrollView
    android:id="@+id/scrollContainer"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent">

    <LinearLayout
        android:id="@+id/wrapper"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/topList"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/bottomList"
            app:layout_constraintStart_toStartOf="parent"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/bottomList"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintTop_toBottomOf="@id/topList"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    </LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

tile_normal.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/normal_layout"
android:layout_width="400px"
android:layout_height="400px"
android:orientation="vertical"
android:gravity="center" >
<TextView
    android:id="@+id/txtTitle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:textSize="25sp" />
</LinearLayout>

tile_wide.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/wide_layout"
android:layout_width="800px"
android:layout_height="400px"
android:orientation="vertical"
android:gravity="center" >
<TextView
    android:id="@+id/txtTitle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:textSize="25sp" />
</LinearLayout>

Solution

  • After much time investigating, it appears that NestedScrollView doesn't actually support horizontal scrolling.

    Scroll view supports vertical scrolling only. For horizontal scrolling, use HorizontalScrollView instead.

    For vertical scrolling, consider NestedScrollView instead of scroll view which offers greater user interface flexibility and support for the material design scrolling patterns.

    Changing my layout by replacing NestedScrollView with HorizontalScrollView, resolves the issue. The revised layout is as such (code changed to use type: NestedScrollView)..

    activity_synced_scroll.xml

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
    android:id="@+id/main_root"
    android:clipToPadding="false"
    android:clipChildren="false">
    
    <androidx.core.widget.NestedScrollView
    android:id="@+id/scrollContainer"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent">
    
    <LinearLayout
        android:id="@+id/wrapper"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/topList"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/bottomList"
            app:layout_constraintStart_toStartOf="parent"/>
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/bottomList"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintTop_toBottomOf="@id/topList"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
        </LinearLayout>
        </androidx.core.widget.NestedScrollView>
    </androidx.constraintlayout.widget.ConstraintLayout>