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 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 
     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     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 
    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 
    243     public void setCloseCallback(CloseCallback callback) {
    244         mCloseCallback = callback;
    245     }
    246 
    247     public void addDragCallback(DragCallback callback) {
    248         mDragCallbacks.add(callback);
    249     }
    250 
    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 {
    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.
    271         void onStartDraggingOpen();
    272 
    273         // Callback in onRestoreInstanceState.
    274         void onInstanceStateRestored(boolean isOpen);
    275 
    276         // Animate the RecyclerView text.
    277         void whileDragging(float yFraction);
    278 
    279         // Whether we should allow the view to be dragged.
    280         boolean shouldCaptureView(View view, int x, int y);
    281 
    282         int getDisplayHeight();
    283     }
    284 
    285     public class DragHelperCallback extends ViewDragHelper.Callback {
    286         @Override
    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
    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
    304         public int getViewVerticalDragRange(View child) {
    305             return mVerticalRange;
    306         }
    307 
    308         @Override
    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
    327         public int clampViewPositionVertical(View child, int top, int dy) {
    328             return Math.max(Math.min(top, 0), -mVerticalRange);
    329         }
    330 
    331         @Override
    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
    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