Why is view dragged with ViewDragHelper reset to its original position on layout()?
Asked Answered
S

6

14

I am implementing a custom expandable action bar in my game. I am using ViewDragHelper to handle the dragging and flinging of the bar view, which is contained in a LinearLayout I subclassed to attach the ViewDragHelper.

The following links have been of great help to achieve this:

The only issue I am encountering is the behavior of the layout() of my parent LinearLayout: on every call to layout() / onLayout(), the child draggable/expandable action bar is reset to its original position (the one set in the XML layout).

Why is that ?

(In my experience layout() never messes with the positions of views that have already been moved)

Syne answered 4/4, 2014 at 14:16 Comment(1)
The problem can be reproduced easily by just adding a button to call requestLayout() on the parent layout of this project: github.com/flavienlaurent/flavienlaurent.com/tree/master/…Rubberize
S
12

The workaround I'm using is to record the view's offsets after each drag operation, and reapply them in onLayout(), e.g.

View mVdhView;
int mVdhXOffset;
int mVdhYOffset;

@Override
public void computeScroll() {
    if (dragHelper.continueSettling(true)) {
        postInvalidateOnAnimation();
    } else {
        mVdhXOffset = mVdhView.getLeft();
        mVdhYOffset = mVdhView.getTop();
    }
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);

      // Reapply VDH offsets
    mVdhView.offsetLeftAndRight(mVdhXOffset);
    mVdhView.offsetTopAndBottom(mVdhYOffset);
}
Seitz answered 22/4, 2014 at 12:31 Comment(0)
D
3

I encountered the same issue when playing with a DrawerLayout (a layout having draggable drawers), and was initially also surprised.

From reading the source, it seems ViewDragHelper uses offsetLeftAndRight() / offsetTopAndBottom() to visually shift any captured and dragged View, presumably to be able to be usable down to API 4.

To answer your question: layout is NOT messing with the positions of the View, it is just that by using the offset..() functions that ViewDragHelper is not hammering down the positioning of the View for you.

So, in your view's onLayout you'll have to account for any visual positioning done using ViewDragHelper. For example, in DrawerLayout this is internally tracked through a simple 'lp.onScreen' percentage field and in its onLayout() the corresponding shift is added to the parameters in a call to child.layout() for any dragged drawer child View.

Deettadeeyn answered 26/8, 2014 at 10:38 Comment(0)
G
3

Sorry I don't know why the layout() is called, but maybe one of your view like ViewPager call getView(View parentView).

Anyway I assume that your using DragViewHelper like this blog describes. So there is the revelant method OnLayoutChangeListener that moves layout to previous position.

MyActivity.java

//parent
rlOuterView = (SlidingLayout) findViewById(R.id.sl_sliding_view);
//child - slides up/down
rlAppBarFooter = (RelativeLayout) findViewById(R.id.rl_footer);
        rlMainView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if( rlOuterView.isMoving() ){
                    v.setTop(oldTop);
                    v.setBottom(oldBottom);
                    v.setLeft(oldLeft);
                    v.setRight(oldRight);
                }
            }
        });

So when is called layout() the method isMoving should be fired up - but it looks like it doesn't. Because DragViewHelper is in STATE_IDLE. I thank for trick to answer by user3452758 . You need to set flag in you custom sliding layout in method computeScroll() , that enable isMoving == true in the right time.

SlidingLayout.java

public class SlidingLayout extends RelativeLayout {
/* ........... */
@Override
    public void computeScroll() { // needed for automatic settling.
        if (mDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
            preventPositionReset = false;
        }else{
            preventPositionReset = true;
        }
    }

    public boolean isMoving() {
        return (/*mDraggingState == ViewDragHelper.STATE_IDLE ||*/
                preventPositionReset ||
                mDraggingState == ViewDragHelper.STATE_DRAGGING ||
                mDraggingState == ViewDragHelper.STATE_SETTLING);
    }
/* ......... */
}
Glanti answered 24/8, 2015 at 18:1 Comment(0)
O
3

This answer is a little late, but the reason why your content gets relayed out to its original position when requestLayout() is called, is (mostly likely) because your code in onLayout() is not taking in to account the changes to the views position.

In flavienlaurent example,his layout code positions the draggable view at a left position of 0 but the top position uses the global scoped variable mTop, which is modified inside of callback onViewPositionChanged(android.view.View, int, int, int, int) to the value of top. You need to track the values of your change and take them into to account inside of onLayout().

A simple way of keeping the position changes bundled with the dragging view is to store them inside of the LayoutParams, and retrieve theme inside of the layout code. This is especially useful if you want to have several draggable views inside of your layout. A excellent example of this pattern is the source code of the Android SDK DrawerLayout (NavigationDrawer). Take a look at the bottom of the class, at the custom LayoutParams inner class. It has a field onScreen that is type float. This variable is used to store the difference in position (relative to the view origin) of the draggable view, and is updated in setDrawerViewOffset().

Another pattern would be to do what user @user3452758 recommended, but again this will get difficult to keep track of if your ever desire to have several draggable views.

Olds answered 1/3, 2016 at 20:34 Comment(0)
W
1

You need just add this code like:

@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top,
                                              int dx, int dy) {
        int right = getMeasuredWidth() - left - changedView.getMeasuredWidth();
        int bottom = getMeasuredHeight() - top - changedView.getMeasuredHeight();
        setLayout(left, top, right, bottom, changedView);
        super.onViewPositionChanged(changedView, left, top, dx, dy);
}
private void setLayout(int l, int t, int r, int b, View dragView) {
        MarginLayoutParams layoutParams = (MarginLayoutParams) dragView.getLayoutParams();
        layoutParams.setMargins(l, t, r, b);
        dragView.setLayoutParams(layoutParams);
}
Wescott answered 3/7, 2020 at 5:42 Comment(0)
S
0

1、in viewDragHelper, record pos when changed

           override fun onViewPositionChanged(
                changedView: View,
                left: Int,
                top: Int,
                dx: Int,
                dy: Int
            ) {
                if (changedView == mFloatView) {
                    mLastDragPos.x = width - mFloatView.right
                    mLastDragPos.y = height - mFloatView.bottom
                }
            }

 override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
// 2、change when onViewReleased 
                if (releasedChild == mFloatView) {
                    releasedChild.let {
                        LogUtils.d(TAG, "onViewReleased")
                        val mr = width - it.right
                        val mb = height - it.bottom
                             ViewUtils.setMargins(
            mFloatView,
            0,
            0,
           mr ,
           mb ,
        )
                    }
                }
            }

int parent view, set child position before onLayout

var mLastDragPos = Point(0, 0)
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
// 3、set child pos first  
        if (mLastDragPos.x > 0 || mLastDragPos.y > 0) {
             ViewUtils.setMargins(
            mFloatView,
            0,
            0,
           mLastDragPos.x,
           mLastDragPos.y,
        )
            mLastDragPos.set(0, 0)
        }
        super.onLayout(changed, left, top, right, bottom)
    }
Soilure answered 31/3, 2023 at 10:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.