1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calculator2; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.PointF; 24 import android.graphics.Rect; 25 import android.os.Bundle; 26 import android.os.Parcelable; 27 import androidx.core.view.ViewCompat; 28 import androidx.customview.widget.ViewDragHelper; 29 import android.util.AttributeSet; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.FrameLayout; 34 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.concurrent.CopyOnWriteArrayList; 39 40 public class DragLayout extends ViewGroup { 41 42 private static final double AUTO_OPEN_SPEED_LIMIT = 600.0; 43 private static final String KEY_IS_OPEN = "IS_OPEN"; 44 private static final String KEY_SUPER_STATE = "SUPER_STATE"; 45 46 private FrameLayout mHistoryFrame; 47 private ViewDragHelper mDragHelper; 48 49 // No concurrency; allow modifications while iterating. 50 private final List<DragCallback> mDragCallbacks = new CopyOnWriteArrayList<>(); 51 private CloseCallback mCloseCallback; 52 53 private final Map<Integer, PointF> mLastMotionPoints = new HashMap<>(); 54 private final Rect mHitRect = new Rect(); 55 56 private int mVerticalRange; 57 private boolean mIsOpen; 58 59 public DragLayout(Context context, AttributeSet attrs) { 60 super(context, attrs); 61 } 62 63 @Override 64 protected void onFinishInflate() { 65 mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); 66 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame); 67 super.onFinishInflate(); 68 } 69 70 @Override 71 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 72 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 73 measureChildren(widthMeasureSpec, heightMeasureSpec); 74 } 75 76 @Override 77 protected void onLayout(boolean changed, int l, int t, int r, int b) { 78 int displayHeight = 0; 79 for (DragCallback c : mDragCallbacks) { 80 displayHeight = Math.max(displayHeight, c.getDisplayHeight()); 81 } 82 mVerticalRange = getHeight() - displayHeight; 83 84 final int childCount = getChildCount(); 85 for (int i = 0; i < childCount; ++i) { 86 final View child = getChildAt(i); 87 88 int top = 0; 89 if (child == mHistoryFrame) { 90 if (mDragHelper.getCapturedView() == mHistoryFrame 91 && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) { 92 top = child.getTop(); 93 } else { 94 top = mIsOpen ? 0 : -mVerticalRange; 95 } 96 } 97 child.layout(0, top, child.getMeasuredWidth(), top + child.getMeasuredHeight()); 98 } 99 } 100 101 @Override 102 protected Parcelable onSaveInstanceState() { 103 final Bundle bundle = new Bundle(); 104 bundle.putParcelable(KEY_SUPER_STATE, super.onSaveInstanceState()); 105 bundle.putBoolean(KEY_IS_OPEN, mIsOpen); 106 return bundle; 107 } 108 109 @Override 110 protected void onRestoreInstanceState(Parcelable state) { 111 if (state instanceof Bundle) { 112 final Bundle bundle = (Bundle) state; 113 mIsOpen = bundle.getBoolean(KEY_IS_OPEN); 114 mHistoryFrame.setVisibility(mIsOpen ? View.VISIBLE : View.INVISIBLE); 115 for (DragCallback c : mDragCallbacks) { 116 c.onInstanceStateRestored(mIsOpen); 117 } 118 119 state = bundle.getParcelable(KEY_SUPER_STATE); 120 } 121 super.onRestoreInstanceState(state); 122 } 123 124 private void saveLastMotion(MotionEvent event) { 125 final int action = event.getActionMasked(); 126 switch (action) { 127 case MotionEvent.ACTION_DOWN: 128 case MotionEvent.ACTION_POINTER_DOWN: { 129 final int actionIndex = event.getActionIndex(); 130 final int pointerId = event.getPointerId(actionIndex); 131 final PointF point = new PointF(event.getX(actionIndex), event.getY(actionIndex)); 132 mLastMotionPoints.put(pointerId, point); 133 break; 134 } 135 case MotionEvent.ACTION_MOVE: { 136 for (int i = event.getPointerCount() - 1; i >= 0; --i) { 137 final int pointerId = event.getPointerId(i); 138 final PointF point = mLastMotionPoints.get(pointerId); 139 if (point != null) { 140 point.set(event.getX(i), event.getY(i)); 141 } 142 } 143 break; 144 } 145 case MotionEvent.ACTION_POINTER_UP: { 146 final int actionIndex = event.getActionIndex(); 147 final int pointerId = event.getPointerId(actionIndex); 148 mLastMotionPoints.remove(pointerId); 149 break; 150 } 151 case MotionEvent.ACTION_UP: 152 case MotionEvent.ACTION_CANCEL: { 153 mLastMotionPoints.clear(); 154 break; 155 } 156 } 157 } 158 159 @Override 160 public boolean onInterceptTouchEvent(MotionEvent event) { 161 saveLastMotion(event); 162 return mDragHelper.shouldInterceptTouchEvent(event); 163 } 164 165 @Override 166 public boolean onTouchEvent(MotionEvent event) { 167 // Workaround: do not process the error case where multi-touch would cause a crash. 168 if (event.getActionMasked() == MotionEvent.ACTION_MOVE 169 && mDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING 170 && mDragHelper.getActivePointerId() != ViewDragHelper.INVALID_POINTER 171 && event.findPointerIndex(mDragHelper.getActivePointerId()) == -1) { 172 mDragHelper.cancel(); 173 return false; 174 } 175 176 saveLastMotion(event); 177 178 mDragHelper.processTouchEvent(event); 179 return true; 180 } 181 182 @Override 183 public void computeScroll() { 184 if (mDragHelper.continueSettling(true)) { 185 ViewCompat.postInvalidateOnAnimation(this); 186 } 187 } 188 189 private void onStartDragging() { 190 for (DragCallback c : mDragCallbacks) { 191 c.onStartDraggingOpen(); 192 } 193 mHistoryFrame.setVisibility(VISIBLE); 194 } 195 196 public boolean isViewUnder(View view, int x, int y) { 197 view.getHitRect(mHitRect); 198 offsetDescendantRectToMyCoords((View) view.getParent(), mHitRect); 199 return mHitRect.contains(x, y); 200 } 201 202 public boolean isMoving() { 203 final int draggingState = mDragHelper.getViewDragState(); 204 return draggingState == ViewDragHelper.STATE_DRAGGING 205 || draggingState == ViewDragHelper.STATE_SETTLING; 206 } 207 208 public boolean isOpen() { 209 return mIsOpen; 210 } 211 212 public void setClosed() { 213 mIsOpen = false; 214 mHistoryFrame.setVisibility(View.INVISIBLE); 215 if (mCloseCallback != null) { 216 mCloseCallback.onClose(); 217 } 218 } 219 220 public Animator createAnimator(boolean toOpen) { 221 if (mIsOpen == toOpen) { 222 return ValueAnimator.ofFloat(0f, 1f).setDuration(0L); 223 } 224 225 mIsOpen = toOpen; 226 mHistoryFrame.setVisibility(VISIBLE); 227 228 final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); 229 animator.addListener(new AnimatorListenerAdapter() { 230 @Override 231 public void onAnimationStart(Animator animation) { 232 mDragHelper.cancel(); 233 mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange); 234 } 235 }); 236 237 return animator; 238 } 239 240 public void setCloseCallback(CloseCallback callback) { 241 mCloseCallback = callback; 242 } 243 244 public void addDragCallback(DragCallback callback) { 245 mDragCallbacks.add(callback); 246 } 247 248 public void removeDragCallback(DragCallback callback) { 249 mDragCallbacks.remove(callback); 250 } 251 252 /** 253 * Callback when the layout is closed. 254 * We use this to pop the HistoryFragment off the backstack. 255 * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on 256 * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the 257 * backstack. 258 */ 259 public interface CloseCallback { 260 void onClose(); 261 } 262 263 /** 264 * Callbacks for coordinating with the RecyclerView or HistoryFragment. 265 */ 266 public interface DragCallback { 267 // Callback when a drag to open begins. 268 void onStartDraggingOpen(); 269 270 // Callback in onRestoreInstanceState. 271 void onInstanceStateRestored(boolean isOpen); 272 273 // Animate the RecyclerView text. 274 void whileDragging(float yFraction); 275 276 // Whether we should allow the view to be dragged. 277 boolean shouldCaptureView(View view, int x, int y); 278 279 int getDisplayHeight(); 280 } 281 282 public class DragHelperCallback extends ViewDragHelper.Callback { 283 @Override 284 public void onViewDragStateChanged(int state) { 285 // The view stopped moving. 286 if (state == ViewDragHelper.STATE_IDLE 287 && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) { 288 setClosed(); 289 } 290 } 291 292 @Override 293 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 294 for (DragCallback c : mDragCallbacks) { 295 // Top is between [-mVerticalRange, 0]. 296 c.whileDragging(1f + (float) top / mVerticalRange); 297 } 298 } 299 300 @Override 301 public int getViewVerticalDragRange(View child) { 302 return mVerticalRange; 303 } 304 305 @Override 306 public boolean tryCaptureView(View view, int pointerId) { 307 final PointF point = mLastMotionPoints.get(pointerId); 308 if (point == null) { 309 return false; 310 } 311 312 final int x = (int) point.x; 313 final int y = (int) point.y; 314 315 for (DragCallback c : mDragCallbacks) { 316 if (!c.shouldCaptureView(view, x, y)) { 317 return false; 318 } 319 } 320 return true; 321 } 322 323 @Override 324 public int clampViewPositionVertical(View child, int top, int dy) { 325 return Math.max(Math.min(top, 0), -mVerticalRange); 326 } 327 328 @Override 329 public void onViewCaptured(View capturedChild, int activePointerId) { 330 super.onViewCaptured(capturedChild, activePointerId); 331 332 if (!mIsOpen) { 333 mIsOpen = true; 334 onStartDragging(); 335 } 336 } 337 338 @Override 339 public void onViewReleased(View releasedChild, float xvel, float yvel) { 340 final boolean settleToOpen; 341 if (yvel > AUTO_OPEN_SPEED_LIMIT) { 342 // Speed has priority over position. 343 settleToOpen = true; 344 } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) { 345 settleToOpen = false; 346 } else { 347 settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2); 348 } 349 350 // If the view is not visible, then settle it closed, not open. 351 if (mDragHelper.settleCapturedViewAt(0, settleToOpen && mIsOpen ? 0 352 : -mVerticalRange)) { 353 ViewCompat.postInvalidateOnAnimation(DragLayout.this); 354 } 355 } 356 } 357 } 358