CoordinatorLayout - flinging in opposite directions causes jittering
I

1

7

I need to make a scrollable fragment with static views at the top and a ViewPager with a RecyclerView in each page at the bottom. I am trying to achieve the desired outcome using a CoordinatorLayout, but ran into a problem - when I fling the top view up (in order to scroll down) and soon after fling RecyclerView down (in order to scroll up), the scrolling is kind of cancelling out and jitters; when I first fling Recycler View down and then the top view up, the whole fragment scrolls back to the top. It looks like the scrolling is carried over and not stopped when I start scrolling in opposite direction.

I found a tutorial with a similar layout (code), but after trying it out, I found out that it suffers from the same issue.

Here are the recordings of the issue:

Edit: added layout code below

fragment_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
​
<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    >
    ​
    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/main.collapsing"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:expandedTitleMarginStart="48dp"
        app:expandedTitleMarginEnd="64dp"
        app:layout_scrollFlags="scroll|snap"
        >
        ​
        <ImageView
            android:id="@+id/materialup.profile_backdrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/picture300x300"
            app:layout_collapseMode="parallax"
            />
    </android.support.design.widget.CollapsingToolbarLayout>
    ​​
    <android.support.v7.widget.Toolbar
        android:id="@+id/materialup.toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="24dp"
        app:theme="@style/ThemeOverlay.AppCompat.Light"
        app:layout_scrollFlags="scroll|enterAlways|snap"
        style="@style/Widget.AppCompat.Toolbar"
        />
    ​
    <LinearLayout
        android:id="@+id/materialup.title_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingTop="8dp"
        android:gravity="center"
        app:layout_scrollFlags="scroll|enterAlways|snap"
        >
        ​
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
            android:text="Title"
            />
        ​
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:layout_marginBottom="4dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle"
            android:text="SubTitle"
            android:textColor="@android:color/white"
            />
        ​
    </LinearLayout>
    ​
    <android.support.design.widget.TabLayout
        android:id="@+id/result_tabs"
        android:layout_width="fill_parent"
        android:layout_height="24dp"
        app:tabSelectedTextColor="?android:attr/textColorPrimaryInverse"
        app:tabIndicatorColor="?android:attr/textColorPrimaryInverse"
        app:tabIndicatorHeight="4dp"
        />
</android.support.design.widget.AppBarLayout>
​
<android.support.v4.view.ViewPager
    android:id="@+id/view_pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    />

MyFragment.java:

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.ewelina.matador.R;

public class MyFragment extends android.support.v4.app.Fragment {

    public static MyFragment newInstance() {
        return new MyFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        final View rootView = inflater.inflate(R.layout.fragment_layout, container, false);

        TabLayout tabLayout = rootView.findViewById(R.id.result_tabs);
        ViewPager viewPager = rootView.findViewById(R.id.view_pager);

        viewPager.setAdapter(new TabsAdapter(getChildFragmentManager()));
        tabLayout.setupWithViewPager(viewPager);

        return rootView;
    }

    private static class TabsAdapter extends FragmentPagerAdapter {
        private static final int TAB_COUNT = 2;

        TabsAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public int getCount() {
            return TAB_COUNT;
        }

        @Override
        public Fragment getItem(int i) {
            return FakePageFragment.newInstance();
        }

        @Override
        public CharSequence getPageTitle(int position) {
            return "Tab " + String.valueOf(position);
        }
    }
}

FakePageFragment.java:

public class FakePageFragment extends Fragment {
    private RecyclerView mRootView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mRootView = (RecyclerView) inflater.inflate(R.layout.fragment_page, container, false);
        return mRootView;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        initRecyclerView();
    }

    private void initRecyclerView() {
        mRootView.setAdapter(new FakePageAdapter(20));
    }

    public static Fragment newInstance() {
        return new FakePageFragment();
    }


    private static class FakePageAdapter extends RecyclerView.Adapter<FakePageVH> {
        private final int numItems;

        FakePageAdapter(int numItems) {
            this.numItems = numItems;
        }

        @Override
        public FakePageVH onCreateViewHolder(ViewGroup viewGroup, int i) {
            View itemView = LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.list_item_card, viewGroup, false);

            return new FakePageVH(itemView);
        }

        @Override
        public void onBindViewHolder(FakePageVH fakePageVH, int i) {
            // do nothing
        }

        @Override
        public int getItemCount() {
            return numItems;
        }
    }

    private static class FakePageVH extends RecyclerView.ViewHolder {
        FakePageVH(View itemView) {
            super(itemView);
        }
    }
}

fragment_page.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
    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:id="@+id/my_recycler_view"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutManager="android.support.v7.widget.LinearLayoutManager"
    tools:listitem="@layout/list_item_card"
    />

list_item_card.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_gravity="center"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_marginRight="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginBottom="4dp"
    android:layout_marginTop="4dp"
    >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fontFamily="sans-serif-light"
        android:padding="8dp"
        android:text="Lorem ipsum"
        android:lineSpacingExtra="4dp"
        android:ellipsize="end"
        />
</LinearLayout>
Insurer answered 30/8, 2018 at 21:50 Comment(2)
This happens with the default recyclerView coordinatorLayout behavior. Have been facing this since a while.Paperboy
Facing the same issue...Humpage
P
0

A similar problem with the fling behavior has been addressed here. A custom behavior needs to be assigned to the appbar.

public final class FlingBehavior extends AppBarLayout.Behavior {
private static final int TOP_CHILD_FLING_THRESHOLD = 3;
private boolean isPositive;

public FlingBehavior() {
}

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

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
    if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
        velocityY = velocityY * -1;
    }
    if (target instanceof RecyclerView && velocityY < 0) {
        final RecyclerView recyclerView = (RecyclerView) target;
        final View firstChild = recyclerView.getChildAt(0);
        final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
        consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
    }
    return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    isPositive = dy > 0;
}
}
Paperboy answered 31/8, 2018 at 8:13 Comment(2)
I implemented that behavior, but to no avail. It seems like it doesn't make any difference at all. The issue in the link you provided is different from mine.Insurer
I concur - the provided solution does not fix the issue.Humpage

© 2022 - 2024 — McMap. All rights reserved.