9

I'm using MotionLayout to build UI with 2 parts - top one with some view and bottom one with SwipeRefresh and RecyclerView inside. Also I have a single gesture for MotionLayout - SwipeRefresh moves up above top view on swipe up. The problem is when I scroll RecyclerView to the bottom (top view "collapses") and then to the top - MotionLayout starts to reverse my transition at once ("expand") - when RecyclerView is not fully scrolled to the top instead of scrolling RecyclerView first. While my SwipeRefresh is updating or refreshing it works as should. Disabling it causes refresh layout progressbar disappearing without animation - it's not a good solution. Any workarounds?

Layout xml gist

Layout scene gist

Andrii Shpek
  • 167
  • 2
  • 10
  • I'm currently having so many problems with motion layout and recyclerview+swipe refresh. One problem is that OnSwipe doesn't work when interfering with recyclerview, even when I specify a target region id. The other, bigger problem, is that some subviews are not redrawn sometimes, mostly when keyboard is opened. Fighting all those problems right now. – frangulyan Apr 03 '20 at 11:37
  • @frangulyan Maybe did you face CoordinatorLayout + Appbar elevation troubles then? I have a semi-transparent view that is collapsed (with translation animation it looks like Recycler overlaps it from the bottom on swipe) and I need shadow when it is collapsed. There is default Appbar elevation, but as my view is transparent, I can see shadow from all 4 sides what is annoying. Also I have a view below collapsed toolbar? like recycler header. Tried to set elevation with clipChildren="false" on AppBar but it didn't work – Andrii Shpek Apr 03 '20 at 15:34
  • I don't event have any of those - no CoordinatorLayout, no AppBars, no Toolbars, no Drawer, just plain full screen views. I have Bottom Navigation though, but it is on the highest level of the activity, the problems I see is in Fragment that is shown by navigation component. An hour ago I opened a question with description of the problems I am having, too much to comment all here - https://stackoverflow.com/questions/61014379/motionlayout-breaks-the-redrawing-of-nested-subviews – frangulyan Apr 03 '20 at 16:05

6 Answers6

15

I had the same issue and came up with a solution while browsing the official bugfix history of MotionLayout. You have to override the onNestedPreScroll method of MotionLayout like this:

/**
 * The current version of motionLayout (2.0.0-beta04) does not honor the position
 * of the RecyclerView, if it is wrapped in a SwipeRefreshLayout.
 * This is the case for the PullRequest screen: When scrolling back to top, the motionLayout transition
 * would be triggered immediately instead of only as soon as the RecyclerView scrolled back to top.
 *
 * This workaround checks if the SwipeRefresh layout can still scroll back up. If so, it does not trigger the motionLayout transition.
 */
class SwipeRefreshMotionLayout : MotionLayout {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (!isInteractionEnabled) {
            return
        }

        if (target !is SwipeRefreshLayout) {
            return super.onNestedPreScroll(target, dx, dy, consumed, type)
        }

        val recyclerView = target.getChildAt(0)
        if (recyclerView !is RecyclerView) {
            return super.onNestedPreScroll(target, dx, dy, consumed, type)
        }

        val canScrollVertically = recyclerView.canScrollVertically(-1)
        if (dy < 0 && canScrollVertically) {
            // don't start motionLayout transition
            return;
        }

        super.onNestedPreScroll(target, dx, dy, consumed, type)
    }
}

Using this MotionLayout in conjunction with SwipeRefreshLayout works nicely for me. I also posted this here, in case you want to keep track of the bugfix by Google.

muetzenflo
  • 5,653
  • 4
  • 41
  • 82
3

I can't post comment to @muetzenflo answer due to lack of reputation, but I was struggling for several hours trying to disable animation in my MotionLayout. I set isInteractionEnabled to "false", but it didn't work. Finally I realized that I use custom MotionLayout and probably should check it. Only when I added

if (!isInteractionEnabled) {
    return
}

as a first check in onNestedPreScroll() disabling animation work as intended.

2

After I set SwipeRefreshLayout as touchAnchorId the bug was gone

<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/swipeContainer"
motion:touchAnchorSide="top" />

androidx.constraintlayout:constraintlayout:2.0.2

Vlkam
  • 53
  • 1
  • 7
1

Adding more to @muetzenflo's answer (as I also don't have enough reputation to comment):

The SwipeRefreshLayout in target will also hold a CircleImageView (which I assume is the refresh icon shown when pulling down). This will sometimes be the first child of the layout (this seems to happen if the fragment the layout is in has been removed and then added back later), so target.getChildAt(0) will return the CircleImageView instead of the RecyclerView. Going through all the children of target and checking if one of them is a RecyclerView provides the expected result.

Code (in Java):

SwipeRefreshLayout swipeLayout = (SwipeRefreshLayout) target;

// Check that the SwipeRefreshLayout has a RecyclerView child
View recyclerView = null;
for (int i = 0; i < swipeLayout.getChildCount(); i++) {
    View child = swipeLayout.getChildAt(i);

    if (child instanceof RecyclerView) {
        recyclerView = child;
        break;
    }
}

if (recyclerView == null) {
    super.onNestedPreScroll(target, dx, dy, consumed, type);
    return;
}
Håkon Schia
  • 931
  • 1
  • 7
  • 12
0

This solution worked for me. This is based on these two answers in java :

https://stackoverflow.com/a/59827320/11962518

https://stackoverflow.com/a/65202877/11962518


binding.motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
            @Override
            public void onTransitionStarted(MotionLayout motionLayout, int startId, int endId) {

                binding.swipeLayout.setEnabled(false);
            }

            @Override
            public void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress) {


                if (progress != 0f) {

                    binding.swipeLayout.setEnabled(false);
                    binding.swipeLayout.setRefreshing(false);
                }
            }

            @Override
            public void onTransitionCompleted(MotionLayout motionLayout, int currentId) {

            }

            @Override
            public void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive, float progress) {


            }
        });

        binding.recyclerView.setOnTouchListener((view, motionEvent) -> {

            binding.motionLayout.onTouchEvent(motionEvent);
            return false;
        });
           

Then override onScrolled method for the RecyclerView like this :

        gridLayoutManager = new RtlGridLayoutManager(context, 1);
        adapterRowList = new RecyclerAdapterRowItemCommodity(rowList.getRowItemHome(), context);
        binding.recyclerView.setAdapter(adapterRowList);
        binding.recyclerView.setLayoutManager(gridLayoutManager);
        binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NotNull RecyclerView recyclerView, int dx, int dy) {


                if (!recyclerView.canScrollVertically(-1)) { //Detects start of RecyclerView
                    binding.swipeLayout.setEnabled(true); // now enable swipe refresh layout 
                }

        });

XML :

Note : You should put your recyclerView inside another layout like this :


 <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/main_lay"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                app:layout_constraintBottom_toTopOf="@id/view_bottom"
                app:layout_constraintTop_toBottomOf="@id/top_buttons">


                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recyclerView"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:paddingStart="@dimen/_8cdp"
                    android:paddingEnd="@dimen/_8cdp"
                    />


            </androidx.constraintlayout.widget.ConstraintLayout>

motion_scene :

Define Transaction like this :

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">

        <OnSwipe motion:touchAnchorId="@id/main_lay"
            motion:touchAnchorSide="top"
            motion:dragDirection="dragUp"/>

    </Transition>

Ghazal
  • 123
  • 2
  • 9
-1

If RecyclerView or ListView is not the direct child of SwipeRefreshLayout then this issue occurs.

Simplest solution is to provide OnChildScrollUpCallback implementation and return the results appropriately. In Kotlin code below, refreshLayout is SwipeRefreshLayout and recyclerView is RecyclerView as can be seen in xml layout code as well.

refreshLayout.setOnChildScrollUpCallback(object : SwipeRefreshLayout.OnChildScrollUpCallback {
  override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?): Boolean {
    if (recyclerView != null) {
      return recyclerView.canScrollVertically(-1)
    }
    return false
  }
})

While xml layout is something like this,

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipeRefresh".../>
    ...
    lots of other views i.e TextView, ImageView, MotionLayout
    ...
    ...
    ...
    <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recyclerView".../>
       
    ...
    ...
    ...
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

This is also answered here.

Sabeeh
  • 1,123
  • 9
  • 11