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 android.support.v4.view.ViewCompat; 28 import android.support.v4.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 DragLayout(Context context, AttributeSet attrs)59 public DragLayout(Context context, AttributeSet attrs) { 60 super(context, attrs); 61 } 62 63 @Override onFinishInflate()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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)71 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 72 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 73 measureChildren(widthMeasureSpec, heightMeasureSpec); 74 } 75 76 @Override onLayout(boolean changed, int l, int t, int r, int b)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 onSaveInstanceState()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 onRestoreInstanceState(Parcelable state)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 saveLastMotion(MotionEvent event)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 onInterceptTouchEvent(MotionEvent event)160 public boolean onInterceptTouchEvent(MotionEvent event) { 161 saveLastMotion(event); 162 return mDragHelper.shouldInterceptTouchEvent(event); 163 } 164 165 @Override onTouchEvent(MotionEvent event)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 computeScroll()183 public void computeScroll() { 184 if (mDragHelper.continueSettling(true)) { 185 ViewCompat.postInvalidateOnAnimation(this); 186 } 187 } 188 onStartDragging()189 private void onStartDragging() { 190 for (DragCallback c : mDragCallbacks) { 191 c.onStartDraggingOpen(); 192 } 193 mHistoryFrame.setVisibility(VISIBLE); 194 } 195 isViewUnder(View view, int x, int y)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 isMoving()202 public boolean isMoving() { 203 final int draggingState = mDragHelper.getViewDragState(); 204 return draggingState == ViewDragHelper.STATE_DRAGGING 205 || draggingState == ViewDragHelper.STATE_SETTLING; 206 } 207 isOpen()208 public boolean isOpen() { 209 return mIsOpen; 210 } 211 setClosed()212 private void setClosed() { 213 if (mIsOpen) { 214 mIsOpen = false; 215 mHistoryFrame.setVisibility(View.INVISIBLE); 216 217 if (mCloseCallback != null) { 218 mCloseCallback.onClose(); 219 } 220 } 221 } 222 createAnimator(boolean toOpen)223 public Animator createAnimator(boolean toOpen) { 224 if (mIsOpen == toOpen) { 225 return ValueAnimator.ofFloat(0f, 1f).setDuration(0L); 226 } 227 228 mIsOpen = toOpen; 229 mHistoryFrame.setVisibility(VISIBLE); 230 231 final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); 232 animator.addListener(new AnimatorListenerAdapter() { 233 @Override 234 public void onAnimationStart(Animator animation) { 235 mDragHelper.cancel(); 236 mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange); 237 } 238 }); 239 240 return animator; 241 } 242 setCloseCallback(CloseCallback callback)243 public void setCloseCallback(CloseCallback callback) { 244 mCloseCallback = callback; 245 } 246 addDragCallback(DragCallback callback)247 public void addDragCallback(DragCallback callback) { 248 mDragCallbacks.add(callback); 249 } 250 removeDragCallback(DragCallback callback)251 public void removeDragCallback(DragCallback callback) { 252 mDragCallbacks.remove(callback); 253 } 254 255 /** 256 * Callback when the layout is closed. 257 * We use this to pop the HistoryFragment off the backstack. 258 * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on 259 * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the 260 * backstack. 261 */ 262 public interface CloseCallback { onClose()263 void onClose(); 264 } 265 266 /** 267 * Callbacks for coordinating with the RecyclerView or HistoryFragment. 268 */ 269 public interface DragCallback { 270 // Callback when a drag to open begins. onStartDraggingOpen()271 void onStartDraggingOpen(); 272 273 // Callback in onRestoreInstanceState. onInstanceStateRestored(boolean isOpen)274 void onInstanceStateRestored(boolean isOpen); 275 276 // Animate the RecyclerView text. whileDragging(float yFraction)277 void whileDragging(float yFraction); 278 279 // Whether we should allow the view to be dragged. shouldCaptureView(View view, int x, int y)280 boolean shouldCaptureView(View view, int x, int y); 281 getDisplayHeight()282 int getDisplayHeight(); 283 } 284 285 public class DragHelperCallback extends ViewDragHelper.Callback { 286 @Override onViewDragStateChanged(int state)287 public void onViewDragStateChanged(int state) { 288 // The view stopped moving. 289 if (state == ViewDragHelper.STATE_IDLE 290 && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) { 291 setClosed(); 292 } 293 } 294 295 @Override onViewPositionChanged(View changedView, int left, int top, int dx, int dy)296 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 297 for (DragCallback c : mDragCallbacks) { 298 // Top is between [-mVerticalRange, 0]. 299 c.whileDragging(1f + (float) top / mVerticalRange); 300 } 301 } 302 303 @Override getViewVerticalDragRange(View child)304 public int getViewVerticalDragRange(View child) { 305 return mVerticalRange; 306 } 307 308 @Override tryCaptureView(View view, int pointerId)309 public boolean tryCaptureView(View view, int pointerId) { 310 final PointF point = mLastMotionPoints.get(pointerId); 311 if (point == null) { 312 return false; 313 } 314 315 final int x = (int) point.x; 316 final int y = (int) point.y; 317 318 for (DragCallback c : mDragCallbacks) { 319 if (!c.shouldCaptureView(view, x, y)) { 320 return false; 321 } 322 } 323 return true; 324 } 325 326 @Override clampViewPositionVertical(View child, int top, int dy)327 public int clampViewPositionVertical(View child, int top, int dy) { 328 return Math.max(Math.min(top, 0), -mVerticalRange); 329 } 330 331 @Override onViewCaptured(View capturedChild, int activePointerId)332 public void onViewCaptured(View capturedChild, int activePointerId) { 333 super.onViewCaptured(capturedChild, activePointerId); 334 335 if (!mIsOpen) { 336 mIsOpen = true; 337 onStartDragging(); 338 } 339 } 340 341 @Override onViewReleased(View releasedChild, float xvel, float yvel)342 public void onViewReleased(View releasedChild, float xvel, float yvel) { 343 final boolean settleToOpen; 344 if (yvel > AUTO_OPEN_SPEED_LIMIT) { 345 // Speed has priority over position. 346 settleToOpen = true; 347 } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) { 348 settleToOpen = false; 349 } else { 350 settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2); 351 } 352 353 if (mDragHelper.settleCapturedViewAt(0, settleToOpen ? 0 : -mVerticalRange)) { 354 ViewCompat.postInvalidateOnAnimation(DragLayout.this); 355 } 356 } 357 } 358 } 359