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.