/* * 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 mDragCallbacks = new CopyOnWriteArrayList<>(); private CloseCallback mCloseCallback; private final Map 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); } } } }