Home | History | Annotate | Download | only in calculator2
      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