/*
|
* Copyright (C) 2016 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.calculator2;
|
|
import android.animation.Animator;
|
import android.animation.AnimatorListenerAdapter;
|
import android.animation.ValueAnimator;
|
import android.content.Context;
|
import android.graphics.PointF;
|
import android.graphics.Rect;
|
import android.os.Bundle;
|
import android.os.Parcelable;
|
import androidx.core.view.ViewCompat;
|
import androidx.customview.widget.ViewDragHelper;
|
import android.util.AttributeSet;
|
import android.view.MotionEvent;
|
import android.view.View;
|
import android.view.ViewGroup;
|
import android.widget.FrameLayout;
|
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
public class DragLayout extends ViewGroup {
|
|
private static final double AUTO_OPEN_SPEED_LIMIT = 600.0;
|
private static final String KEY_IS_OPEN = "IS_OPEN";
|
private static final String KEY_SUPER_STATE = "SUPER_STATE";
|
|
private FrameLayout mHistoryFrame;
|
private ViewDragHelper mDragHelper;
|
|
// No concurrency; allow modifications while iterating.
|
private final List<DragCallback> mDragCallbacks = new CopyOnWriteArrayList<>();
|
private CloseCallback mCloseCallback;
|
|
private final Map<Integer, PointF> mLastMotionPoints = new HashMap<>();
|
private final Rect mHitRect = new Rect();
|
|
private int mVerticalRange;
|
private boolean mIsOpen;
|
|
public DragLayout(Context context, AttributeSet attrs) {
|
super(context, attrs);
|
}
|
|
@Override
|
protected void onFinishInflate() {
|
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
|
mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
|
super.onFinishInflate();
|
}
|
|
@Override
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
measureChildren(widthMeasureSpec, heightMeasureSpec);
|
}
|
|
@Override
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
int displayHeight = 0;
|
for (DragCallback c : mDragCallbacks) {
|
displayHeight = Math.max(displayHeight, c.getDisplayHeight());
|
}
|
mVerticalRange = getHeight() - displayHeight;
|
|
final int childCount = getChildCount();
|
for (int i = 0; i < childCount; ++i) {
|
final View child = getChildAt(i);
|
|
int top = 0;
|
if (child == mHistoryFrame) {
|
if (mDragHelper.getCapturedView() == mHistoryFrame
|
&& mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
|
top = child.getTop();
|
} else {
|
top = mIsOpen ? 0 : -mVerticalRange;
|
}
|
}
|
child.layout(0, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
|
}
|
}
|
|
@Override
|
protected Parcelable onSaveInstanceState() {
|
final Bundle bundle = new Bundle();
|
bundle.putParcelable(KEY_SUPER_STATE, super.onSaveInstanceState());
|
bundle.putBoolean(KEY_IS_OPEN, mIsOpen);
|
return bundle;
|
}
|
|
@Override
|
protected void onRestoreInstanceState(Parcelable state) {
|
if (state instanceof Bundle) {
|
final Bundle bundle = (Bundle) state;
|
mIsOpen = bundle.getBoolean(KEY_IS_OPEN);
|
mHistoryFrame.setVisibility(mIsOpen ? View.VISIBLE : View.INVISIBLE);
|
for (DragCallback c : mDragCallbacks) {
|
c.onInstanceStateRestored(mIsOpen);
|
}
|
|
state = bundle.getParcelable(KEY_SUPER_STATE);
|
}
|
super.onRestoreInstanceState(state);
|
}
|
|
private void saveLastMotion(MotionEvent event) {
|
final int action = event.getActionMasked();
|
switch (action) {
|
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
final int actionIndex = event.getActionIndex();
|
final int pointerId = event.getPointerId(actionIndex);
|
final PointF point = new PointF(event.getX(actionIndex), event.getY(actionIndex));
|
mLastMotionPoints.put(pointerId, point);
|
break;
|
}
|
case MotionEvent.ACTION_MOVE: {
|
for (int i = event.getPointerCount() - 1; i >= 0; --i) {
|
final int pointerId = event.getPointerId(i);
|
final PointF point = mLastMotionPoints.get(pointerId);
|
if (point != null) {
|
point.set(event.getX(i), event.getY(i));
|
}
|
}
|
break;
|
}
|
case MotionEvent.ACTION_POINTER_UP: {
|
final int actionIndex = event.getActionIndex();
|
final int pointerId = event.getPointerId(actionIndex);
|
mLastMotionPoints.remove(pointerId);
|
break;
|
}
|
case MotionEvent.ACTION_UP:
|
case MotionEvent.ACTION_CANCEL: {
|
mLastMotionPoints.clear();
|
break;
|
}
|
}
|
}
|
|
@Override
|
public boolean onInterceptTouchEvent(MotionEvent event) {
|
saveLastMotion(event);
|
return mDragHelper.shouldInterceptTouchEvent(event);
|
}
|
|
@Override
|
public boolean onTouchEvent(MotionEvent event) {
|
// Workaround: do not process the error case where multi-touch would cause a crash.
|
if (event.getActionMasked() == MotionEvent.ACTION_MOVE
|
&& mDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING
|
&& mDragHelper.getActivePointerId() != ViewDragHelper.INVALID_POINTER
|
&& event.findPointerIndex(mDragHelper.getActivePointerId()) == -1) {
|
mDragHelper.cancel();
|
return false;
|
}
|
|
saveLastMotion(event);
|
|
mDragHelper.processTouchEvent(event);
|
return true;
|
}
|
|
@Override
|
public void computeScroll() {
|
if (mDragHelper.continueSettling(true)) {
|
ViewCompat.postInvalidateOnAnimation(this);
|
}
|
}
|
|
private void onStartDragging() {
|
for (DragCallback c : mDragCallbacks) {
|
c.onStartDraggingOpen();
|
}
|
mHistoryFrame.setVisibility(VISIBLE);
|
}
|
|
public boolean isViewUnder(View view, int x, int y) {
|
view.getHitRect(mHitRect);
|
offsetDescendantRectToMyCoords((View) view.getParent(), mHitRect);
|
return mHitRect.contains(x, y);
|
}
|
|
public boolean isMoving() {
|
final int draggingState = mDragHelper.getViewDragState();
|
return draggingState == ViewDragHelper.STATE_DRAGGING
|
|| draggingState == ViewDragHelper.STATE_SETTLING;
|
}
|
|
public boolean isOpen() {
|
return mIsOpen;
|
}
|
|
public void setClosed() {
|
mIsOpen = false;
|
mHistoryFrame.setVisibility(View.INVISIBLE);
|
if (mCloseCallback != null) {
|
mCloseCallback.onClose();
|
}
|
}
|
|
public Animator createAnimator(boolean toOpen) {
|
if (mIsOpen == toOpen) {
|
return ValueAnimator.ofFloat(0f, 1f).setDuration(0L);
|
}
|
|
mIsOpen = toOpen;
|
mHistoryFrame.setVisibility(VISIBLE);
|
|
final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
|
animator.addListener(new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationStart(Animator animation) {
|
mDragHelper.cancel();
|
mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange);
|
}
|
});
|
|
return animator;
|
}
|
|
public void setCloseCallback(CloseCallback callback) {
|
mCloseCallback = callback;
|
}
|
|
public void addDragCallback(DragCallback callback) {
|
mDragCallbacks.add(callback);
|
}
|
|
public void removeDragCallback(DragCallback callback) {
|
mDragCallbacks.remove(callback);
|
}
|
|
/**
|
* Callback when the layout is closed.
|
* We use this to pop the HistoryFragment off the backstack.
|
* We can't use a method in DragCallback because we get ConcurrentModificationExceptions on
|
* mDragCallbacks when executePendingTransactions() is called for popping the fragment off the
|
* backstack.
|
*/
|
public interface CloseCallback {
|
void onClose();
|
}
|
|
/**
|
* Callbacks for coordinating with the RecyclerView or HistoryFragment.
|
*/
|
public interface DragCallback {
|
// Callback when a drag to open begins.
|
void onStartDraggingOpen();
|
|
// Callback in onRestoreInstanceState.
|
void onInstanceStateRestored(boolean isOpen);
|
|
// Animate the RecyclerView text.
|
void whileDragging(float yFraction);
|
|
// Whether we should allow the view to be dragged.
|
boolean shouldCaptureView(View view, int x, int y);
|
|
int getDisplayHeight();
|
}
|
|
public class DragHelperCallback extends ViewDragHelper.Callback {
|
@Override
|
public void onViewDragStateChanged(int state) {
|
// The view stopped moving.
|
if (state == ViewDragHelper.STATE_IDLE
|
&& mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
|
setClosed();
|
}
|
}
|
|
@Override
|
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
|
for (DragCallback c : mDragCallbacks) {
|
// Top is between [-mVerticalRange, 0].
|
c.whileDragging(1f + (float) top / mVerticalRange);
|
}
|
}
|
|
@Override
|
public int getViewVerticalDragRange(View child) {
|
return mVerticalRange;
|
}
|
|
@Override
|
public boolean tryCaptureView(View view, int pointerId) {
|
final PointF point = mLastMotionPoints.get(pointerId);
|
if (point == null) {
|
return false;
|
}
|
|
final int x = (int) point.x;
|
final int y = (int) point.y;
|
|
for (DragCallback c : mDragCallbacks) {
|
if (!c.shouldCaptureView(view, x, y)) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
@Override
|
public int clampViewPositionVertical(View child, int top, int dy) {
|
return Math.max(Math.min(top, 0), -mVerticalRange);
|
}
|
|
@Override
|
public void onViewCaptured(View capturedChild, int activePointerId) {
|
super.onViewCaptured(capturedChild, activePointerId);
|
|
if (!mIsOpen) {
|
mIsOpen = true;
|
onStartDragging();
|
}
|
}
|
|
@Override
|
public void onViewReleased(View releasedChild, float xvel, float yvel) {
|
final boolean settleToOpen;
|
if (yvel > AUTO_OPEN_SPEED_LIMIT) {
|
// Speed has priority over position.
|
settleToOpen = true;
|
} else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
|
settleToOpen = false;
|
} else {
|
settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
|
}
|
|
// If the view is not visible, then settle it closed, not open.
|
if (mDragHelper.settleCapturedViewAt(0, settleToOpen && mIsOpen ? 0
|
: -mVerticalRange)) {
|
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
|
}
|
}
|
}
|
}
|