0

I have a RelativeLayout which has a ViewDragHelper which I use to move the view around. I have to use layout(boolean changed, int left, int top, int right, int bottom) to update view size on drag because the views inside need to resize. This all works fine. Now the child views inside have their touch positions all wrong. Buttons work as if they were on top of the screen as opposed to where they are.

private void changeDragViewPosition(int top, float verticalMovementFactor) {
    int right = calculateViewRightPosition(verticalMovementFactor);
    int left = right - mainViewLayoutParams.width;
    int bottom = top + mainViewLayoutParams.height;

    mainView.layout(left, top, right, bottom);
}

This is the code I use for moving the view. The mainView itself has correct touch positions, the child views inside this view are the problem. Is there anything I'm missing?

EDIT

Here is the view i'm using.

public class MinimizableView extends RelativeLayout {

private static final int DEFAULT_MINIMIZED_MARGIN = 2;
private static final int DEFAULT_SCALE_FACTOR = 2;
private static final int MIN_SLIDING_DISTANCE_ON_CLICK = 10;

private View mainView;
private View unmovableView;
private ArrayList<View> otherViews;
private ArrayList<Integer> initialPositions;

private LayoutParams mainViewLayoutParams;

private int verticalDragRange;
private int mainViewOriginalWidth;
private int mainViewOriginalHeight;
private float scaleFactor = DEFAULT_SCALE_FACTOR;
private float minimizedMargin;
private boolean firstLayoutPass = true;
private int mainViewInitialPosition;
private float lastTouchActionDownXPosition;

private ViewDragHelper viewDragHelper;

private MinimizableViewListener listener;

public interface MinimizableViewListener {
    void onMinimized();

    void onMaximized();

    void onClosed();
}

public MinimizableView(Context context) {
    super(context);

    init(context);
}

public MinimizableView(Context context, AttributeSet attrs) {
    super(context, attrs);

    init(context);
}

public MinimizableView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    init(context);
}

private void init(Context context) {
    minimizedMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MINIMIZED_MARGIN, getResources().getDisplayMetrics());

    ViewCompat.requestApplyInsets(this);
}

private ViewDragHelper.Callback viewDragHelperCallback = new ViewDragHelper.Callback() {

    private static final int MINIMUM_DX_FOR_HORIZONTAL_DRAG = 5;
    private static final int MINIMUM_DY_FOR_VERTICAL_DRAG = 15;
    private static final float X_MIN_VELOCITY = 1500;
    private static final float Y_MIN_VELOCITY = 1000;

    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return child.equals(mainView);
    }

    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        if (!isMainViewAtBottom()) {
            float verticalMovementFactor = (top - mainViewInitialPosition) / (float) verticalDragRange;

            changeDragViewScale(verticalMovementFactor);
            changeDragViewPosition(top, verticalMovementFactor);
            changeSecondViewAlpha(verticalMovementFactor);
            changeSecondViewPosition(verticalMovementFactor);
            changeUnmovableViewAlpha(verticalMovementFactor);
        }
    }

    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);

        if (isMainViewAtBottom() && !isViewAtRight(releasedChild)) {
            triggerOnReleaseActionsWhileHorizontalDrag(xvel);
        } else {
            triggerOnReleaseActionsWhileVerticalDrag(yvel);
        }
    }

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        int newTop = verticalDragRange + mainViewInitialPosition;
        if (isMinimized() && Math.abs(dy) >= MINIMUM_DY_FOR_VERTICAL_DRAG || (!isMinimized() && !isMainViewAtBottom())) {
            final int topBound = getPaddingTop() + mainViewInitialPosition;
            final int bottomBound = verticalDragRange + mainViewInitialPosition;

            newTop = Math.min(Math.max(top, topBound), bottomBound);
        }
        return newTop;
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        int newLeft = mainView.getLeft();
        if ((isMinimized() && Math.abs(dx) > MINIMUM_DX_FOR_HORIZONTAL_DRAG) || (isMainViewAtBottom() && !isViewAtRight(mainView))) {
            newLeft = left;
        }
        return newLeft;
    }

    private void triggerOnReleaseActionsWhileHorizontalDrag(float xvel) {
        if (xvel < 0 && xvel <= -X_MIN_VELOCITY) {
            closeToLeft();
        } else if (xvel > 0 && xvel >= X_MIN_VELOCITY) {
            closeToRight();
        } else {
            if (isNextToLeftBound(mainView)) {
                closeToLeft();
            } else if (isNextToRightBound(mainView)) {
                closeToRight();
            } else {
                minimize();
            }
        }
    }

    private void triggerOnReleaseActionsWhileVerticalDrag(float xvel) {
        if (xvel < 0 && xvel <= -Y_MIN_VELOCITY) {
            maximize();
        } else if (xvel > 0 && xvel >= Y_MIN_VELOCITY) {
            minimize();
        } else {
            if (isDragViewAboveTheMiddle(mainView)) {
                maximize();
            } else {
                minimize();
            }
        }
    }
};

private void changeDragViewScale(float verticalMovementFactor) {
    mainViewLayoutParams.width = (int) (mainViewOriginalWidth * (1 - (verticalMovementFactor / scaleFactor)));
    mainViewLayoutParams.height = (int) (mainViewOriginalHeight * (1 - (verticalMovementFactor / scaleFactor)));

    mainView.setLayoutParams(mainViewLayoutParams);
}

private void changeDragViewPosition(int top, float verticalMovementFactor) {
    int right = calculateViewRightPosition(verticalMovementFactor);
    int left = right - mainViewLayoutParams.width;
    int bottom = top + mainViewLayoutParams.height;

    mainView.layout(left, top, right, bottom);
}

private void changeSecondViewAlpha(float verticalMovementFactor) {
    for (int i = 0; i < otherViews.size(); i++) {
        otherViews.get(i).setAlpha(1 - verticalMovementFactor);
    }
}

private void changeSecondViewPosition(float verticalMovementFactor) {
    int newTop;
    int initialTop;
    for (int i = 0; i < otherViews.size(); i++) {
        initialTop = initialPositions.get(i);
        newTop = (int) (initialTop + ((getBottom() - initialTop) * verticalMovementFactor));

        otherViews.get(i).setY(newTop);
    }
}

private void changeUnmovableViewAlpha(float verticalMovementFactor) {
    unmovableView.setAlpha(1 - verticalMovementFactor);
}

private int calculateViewRightPosition(float verticalMoveFactor) {
    return (int) (mainViewOriginalWidth - minimizedMargin * verticalMoveFactor);
}

private boolean isDragViewAboveTheMiddle(View view) {
    int parentHeight = getHeight();
    float viewYPosition = view.getY() + (view.getHeight() * 0.5f);

    return viewYPosition < (parentHeight * 0.5);
}

private boolean isMainViewAtTop() {
    return mainView.getTop() == mainViewInitialPosition;
}

private boolean isMainViewAtBottom() {
    return mainView.getBottom() >= getBottom() - getPaddingBottom() - minimizedMargin - 1;
}

private boolean isViewAtRight(View view) {
    return view.getRight() + minimizedMargin + 10 >= getWidth() - 10;
}

private void closeToLeft() {
    if (viewDragHelper.smoothSlideViewTo(mainView, -mainViewOriginalWidth, getHeight() - getMinHeightPlusMargin())) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    if (listener != null) {
        listener.onClosed();
    }
}

private int getMinHeightPlusMargin() {
    return (int) (mainViewOriginalHeight * (1 - 1 / scaleFactor) + minimizedMargin);
}

private int getMinWidthPlusMargin() {
    return (int) (mainViewOriginalWidth * (1 - 1 / scaleFactor) + minimizedMargin);
}

private void closeToRight() {
    if (viewDragHelper.smoothSlideViewTo(mainView, mainViewOriginalWidth, getHeight() - getMinHeightPlusMargin())) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    if (listener != null) {
        listener.onClosed();
    }
}

private boolean isNextToLeftBound(View view) {
    return (view.getLeft() - minimizedMargin) < getWidth() * 0.05;
}

private boolean isNextToRightBound(View view) {
    return (view.getLeft() - minimizedMargin) > getWidth() * 0.75;
}

private boolean isViewHit(View view, int x, int y) {
    int[] viewLocation = new int[2];
    view.getLocationOnScreen(viewLocation);
    int[] parentLocation = new int[2];
    this.getLocationOnScreen(parentLocation);
    int screenX = parentLocation[0] + x;
    int screenY = parentLocation[1] + y;
    return screenX >= viewLocation[0]
            && screenX < viewLocation[0] + view.getWidth()
            && screenY >= viewLocation[1]
            && screenY < viewLocation[1] + view.getHeight();
}

private static final int INVALID_POINTER = -1;

private int activePointerId;

@Override
public boolean onTouchEvent(MotionEvent event) {
    int actionMasked = MotionEventCompat.getActionMasked(event);
    if ((actionMasked & MotionEventCompat.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
        activePointerId = MotionEventCompat.getPointerId(event, actionMasked);
    }
    if (activePointerId == INVALID_POINTER) {
        return false;
    }
    viewDragHelper.processTouchEvent(event);
    if (isClosed()) {
        return false;
    }
    boolean isDragViewHit = isViewHit(mainView, (int) event.getX(), (int) event.getY());
    boolean isSecondViewHit = false;
    for (int i = 0; i < otherViews.size(); i++) {
        if (isViewHit(otherViews.get(i), (int) event.getX(), (int) event.getY())) {
            isSecondViewHit = true;
            break;
        }
    }
    analyzeTouchToMaximizeIfNeeded(event, isDragViewHit);
    if (isMaximized()) {
        mainView.dispatchTouchEvent(event);
    } else {
        mainView.dispatchTouchEvent(cloneMotionEventWithAction(event, MotionEvent.ACTION_CANCEL));
    }
    return isDragViewHit || isSecondViewHit;
}

private void analyzeTouchToMaximizeIfNeeded(MotionEvent ev, boolean isDragViewHit) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastTouchActionDownXPosition = ev.getX();
            break;
        case MotionEvent.ACTION_UP:
            float clickOffset = ev.getX() - lastTouchActionDownXPosition;
            if (shouldMaximizeOnClick(ev, clickOffset, isDragViewHit)) {
                if (isMinimized()) {
                    maximize();
                }
            }
            break;
        default:
            break;
    }
}

public boolean shouldMaximizeOnClick(MotionEvent ev, float deltaX, boolean isDragViewHit) {
    return (Math.abs(deltaX) < MIN_SLIDING_DISTANCE_ON_CLICK) && ev.getAction() != MotionEvent.ACTION_MOVE && isDragViewHit;
}

private MotionEvent cloneMotionEventWithAction(MotionEvent event, int action) {
    return MotionEvent.obtain(event.getDownTime(), event.getEventTime(), action, event.getX(), event.getY(), event.getMetaState());
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (!isEnabled()) {
        return false;
    }
    switch (MotionEventCompat.getActionMasked(ev) & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            viewDragHelper.cancel();
            return false;
        case MotionEvent.ACTION_DOWN:
            int index = MotionEventCompat.getActionIndex(ev);
            activePointerId = MotionEventCompat.getPointerId(ev, index);
            if (activePointerId == INVALID_POINTER) {
                return false;
            }
            break;
        default:
            break;
    }
    boolean interceptTap = viewDragHelper.isViewUnder(mainView, (int) ev.getX(), (int) ev.getY());
    return viewDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}

@Override
protected void onFinishInflate() {
    super.onFinishInflate();

    otherViews = new ArrayList<>();
    initialPositions = new ArrayList<>();

    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child instanceof DragView) {
            mainView = child;
        } else if (child instanceof UnmovableView) {
            unmovableView = child;
        } else {
            otherViews.add(child);
        }
    }

    viewDragHelper = ViewDragHelper.create(this, 1, viewDragHelperCallback);

    mainViewLayoutParams = (LayoutParams) mainView.getLayoutParams();
}

@Override
public void computeScroll() {
    if (!isInEditMode() && viewDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

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

        mainViewInitialPosition = mainView.getTop();
        verticalDragRange = getMeasuredHeight() - mainViewInitialPosition - getMinHeightPlusMargin() - getPaddingTop() - getPaddingBottom();
        for (int i = 0; i < otherViews.size(); i++) {
            initialPositions.add(otherViews.get(i).getTop());
        }

        firstLayoutPass = false;
    } else if (isMainViewAtTop()) {
        mainView.layout(left, mainViewInitialPosition, right, mainViewInitialPosition + mainViewOriginalHeight);

        changeUnmovableViewAlpha(0);

        setLayoutPositions(0, left, right, bottom, true);
    } else {
        setLayoutPositions(mainView.getTop() / (float) verticalDragRange, left, right, bottom, false);
    }
}

private void setLayoutPositions(float offsetFactor, int left, int right, int bottom, boolean setY) {
    int newTop;
    int initialTop;
    for (int i = 0; i < otherViews.size(); i++) {
        View view = otherViews.get(i);

        initialTop = initialPositions.get(i);
        newTop = (int) (initialTop + ((bottom - initialTop) * offsetFactor));

        view.layout(left, newTop, right, newTop + view.getHeight());
        if (setY) {
            view.setY(newTop);
        }
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (mainViewOriginalWidth == 0) {
        mainViewOriginalWidth = mainView.getMeasuredWidth();
        mainViewOriginalHeight = mainView.getMeasuredHeight();
    }
}

private boolean smoothSlideTo(float slideOffset) {
    final int topBound = mainViewInitialPosition + getPaddingTop();
    int x = (int) (slideOffset * (getWidth() - getMinWidthPlusMargin()));
    int y = (int) ((slideOffset * verticalDragRange) + topBound);
    if (viewDragHelper.smoothSlideViewTo(mainView, x, y)) {
        ViewCompat.postInvalidateOnAnimation(this);
        return true;
    }
    return false;
}

@Override
public void setPadding(int left, int top, int right, int bottom) {
    super.setPadding(left, top, right, bottom);

    mainViewInitialPosition = mainView.getTop();
    verticalDragRange = getMeasuredHeight() - mainViewInitialPosition - getMinHeightPlusMargin() - getPaddingTop() - getPaddingBottom();
    for (int i = 0; i < otherViews.size(); i++) {
        initialPositions.add(otherViews.get(i).getTop());
    }
}

// --------- PUBLIC METHODS ---------- //
public void maximize() {
    smoothSlideTo(0);
    if (listener != null) {
        listener.onMaximized();
    }
}

public void minimize() {
    smoothSlideTo(1);
    if (listener != null) {
        listener.onMinimized();
    }
}

public boolean isMinimized() {
    return isMainViewAtBottom() && isViewAtRight(mainView);
}

public boolean isMaximized() {
    return mainView.getTop() == mainViewInitialPosition;
}

public boolean isClosed() {
    return mainView.getRight() <= 0 && mainView.getLeft() >= getWidth();
}

public void setMinimizableViewListener(MinimizableViewListener listener) {
    this.listener = listener;
}

public void hide() {
    changeDragViewScale(1);
    changeDragViewPosition(verticalDragRange + mainViewInitialPosition - getMinHeightPlusMargin(), 1);
    minimize();
    setVisibility(GONE);
}

public void show() {
    setVisibility(VISIBLE);
    post(new Runnable() {
        @Override
        public void run() {
            maximize();
        }
    });
}

}

As you can see the I have overridden the onTouch method and it works fine. I also have a button inside the mainView which is also a RelativeLayout.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lycatv.dragviewtest.MainActivity">

<android.support.v7.widget.Toolbar
    android:id="@+id/actionBar1"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary" />

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/actionBar1"
    android:fitsSystemWindows="true"
    android:text="Hello World!" />

<com.lycatv.dragviewtest.MinimizableView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="56dp">

    <com.lycatv.dragviewtest.UnmovableView
        android:id="@+id/actionBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimaryDark" />
    </com.lycatv.dragviewtest.UnmovableView>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/actionBar"
        android:background="#1a1e39" />

    <FrameLayout
        android:id="@+id/frameLayout1"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_below="@+id/actionBar" />

    <com.lycatv.dragviewtest.DragView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/frameLayout1">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:src="#000000" />

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:text="CheckBox"
            android:textColor="@android:color/white" />
    </com.lycatv.dragviewtest.DragView>
</com.lycatv.dragviewtest.MinimizableView>

The CheckBox does not work when you press on it. Instead it works when you press above it. By whatever amount it is offset from the top.

  • I'm not sure if i understand well. You have a RelativeLayout and inside you have some views. When you move the RelativeLayout you want to resize views inside it, right? Then you talk about buttons, what buttons? could you post maybe an image or try to explain it a bit easier? sorry and thanks – Hugo Nov 03 '16 at 16:55
  • @Hugo I have added the whole code – Naveen Dissanayake Nov 04 '16 at 05:20

1 Answers1

1

I won't pretend to understand everything your code is doing. However, I did notice something in onLayout that is almost certainly wrong. The code you posted is this:

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

        mainViewInitialPosition = mainView.getTop();
        verticalDragRange = getMeasuredHeight() - mainViewInitialPosition - getMinHeightPlusMargin() - getPaddingTop() - getPaddingBottom();
        for (int i = 0; i < otherViews.size(); i++) {
            initialPositions.add(otherViews.get(i).getTop());
        }

        firstLayoutPass = false;
    } else if (isMainViewAtTop()) {
        mainView.layout(left, mainViewInitialPosition, right, mainViewInitialPosition + mainViewOriginalHeight);

        changeUnmovableViewAlpha(0);

        setLayoutPositions(0, left, right, bottom, true);
    } else {
        setLayoutPositions(mainView.getTop() / (float) verticalDragRange, left, right, bottom, false);
    }
}

The almost-certain error in this code is in your use of the layout method here:

mainView.layout(left, mainViewInitialPosition, right, mainViewInitialPosition + mainViewOriginalHeight);

You are using the parameter values left and right to layout your mainView (and the child views via setLayoutPositions). This is wrong. Keep in mind that left, top, right and bottom are the relative positions of your parent view (MinimizableView); they should not be used directly to layout your child views, as the child views are relative to MinimizableView, not to the parent of MinimizableView.

For example, when you layout mainView, the values you pass into layout() are interpreted as being relative to MinimizableView. This means the left offset should be 0 (plus padding), not left. Diddo for right. You layout call should look more like:

mainView.layout(0, 0, mainView.getMeasuredWidth(), mainView.getMeasuredHeight());

Note that the above does not account for padding.

pathfinderelite
  • 3,047
  • 1
  • 27
  • 30
  • Thank you for taking to time to look at this. I can set left as 0 but I cannot set top as 0 because that would move the view to the top. I need the `mainView` to stay where it started. The `setLayoutPositions` method is just a helper to move the `otherViews` inside parent. And the placement is not the issue here its the touch position. But I will try moving these values around. – Naveen Dissanayake Nov 05 '16 at 14:35