Home | History | Annotate | Download | only in bubble
      1 /*
      2  * Copyright (C) 2017 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.dialershared.bubble;
     18 
     19 import android.content.Context;
     20 import android.graphics.Point;
     21 import android.support.animation.FloatPropertyCompat;
     22 import android.support.animation.SpringAnimation;
     23 import android.support.animation.SpringForce;
     24 import android.support.annotation.NonNull;
     25 import android.support.v4.math.MathUtils;
     26 import android.view.Gravity;
     27 import android.view.MotionEvent;
     28 import android.view.VelocityTracker;
     29 import android.view.View;
     30 import android.view.View.OnTouchListener;
     31 import android.view.ViewConfiguration;
     32 import android.view.WindowManager;
     33 import android.view.WindowManager.LayoutParams;
     34 import android.widget.Scroller;
     35 
     36 /** Handles touches and manages moving the bubble in response */
     37 class MoveHandler implements OnTouchListener {
     38 
     39   // Amount the ViewConfiguration's minFlingVelocity will be scaled by for our own minVelocity
     40   private static final int MIN_FLING_VELOCITY_FACTOR = 8;
     41   // The friction multiplier to control how slippery the bubble is when flung
     42   private static final float SCROLL_FRICTION_MULTIPLIER = 4f;
     43 
     44   private final Context context;
     45   private final WindowManager windowManager;
     46   private final Bubble bubble;
     47   private final int minX;
     48   private final int minY;
     49   private final int maxX;
     50   private final int maxY;
     51   private final int bubbleSize;
     52   private final int shadowPaddingSize;
     53   private final float touchSlopSquared;
     54 
     55   private boolean isMoving;
     56   private float firstX;
     57   private float firstY;
     58 
     59   private SpringAnimation moveXAnimation;
     60   private SpringAnimation moveYAnimation;
     61   private VelocityTracker velocityTracker;
     62   private Scroller scroller;
     63 
     64   // Handles the left/right gravity conversion and centering
     65   private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty =
     66       new FloatPropertyCompat<LayoutParams>("xProperty") {
     67         @Override
     68         public float getValue(LayoutParams windowParams) {
     69           int realX = windowParams.x;
     70           realX = realX + bubbleSize / 2;
     71           realX = realX + shadowPaddingSize;
     72           if (relativeToRight(windowParams)) {
     73             int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
     74             realX = displayWidth - realX;
     75           }
     76           return MathUtils.clamp(realX, minX, maxX);
     77         }
     78 
     79         @Override
     80         public void setValue(LayoutParams windowParams, float value) {
     81           boolean wasOnRight = (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
     82           int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
     83           boolean onRight;
     84           Integer gravityOverride = bubble.getGravityOverride();
     85           if (gravityOverride == null) {
     86             onRight = value > displayWidth / 2;
     87           } else {
     88             onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT;
     89           }
     90           int centeringOffset = bubbleSize / 2 + shadowPaddingSize;
     91           windowParams.x =
     92               (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset);
     93           windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT);
     94           if (wasOnRight != onRight) {
     95             bubble.onLeftRightSwitch(onRight);
     96           }
     97           if (bubble.isVisible()) {
     98             windowManager.updateViewLayout(bubble.getRootView(), windowParams);
     99           }
    100         }
    101       };
    102 
    103   private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty =
    104       new FloatPropertyCompat<LayoutParams>("yProperty") {
    105         @Override
    106         public float getValue(LayoutParams object) {
    107           return MathUtils.clamp(object.y + bubbleSize + shadowPaddingSize, minY, maxY);
    108         }
    109 
    110         @Override
    111         public void setValue(LayoutParams object, float value) {
    112           object.y = (int) value - bubbleSize - shadowPaddingSize;
    113           if (bubble.isVisible()) {
    114             windowManager.updateViewLayout(bubble.getRootView(), object);
    115           }
    116         }
    117       };
    118 
    119   public MoveHandler(@NonNull View targetView, @NonNull Bubble bubble) {
    120     this.bubble = bubble;
    121     context = targetView.getContext();
    122     windowManager = context.getSystemService(WindowManager.class);
    123 
    124     bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
    125     shadowPaddingSize =
    126         context.getResources().getDimensionPixelOffset(R.dimen.bubble_shadow_padding_size);
    127     minX =
    128         context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_x)
    129             + bubbleSize / 2;
    130     minY =
    131         context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_y)
    132             + bubbleSize / 2;
    133     maxX = context.getResources().getDisplayMetrics().widthPixels - minX;
    134     maxY = context.getResources().getDisplayMetrics().heightPixels - minY;
    135 
    136     // Squared because it will be compared against the square of the touch delta. This is more
    137     // efficient than needing to take a square root.
    138     touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
    139 
    140     targetView.setOnTouchListener(this);
    141   }
    142 
    143   public boolean isMoving() {
    144     return isMoving;
    145   }
    146 
    147   public void undoGravityOverride() {
    148     LayoutParams windowParams = bubble.getWindowParams();
    149     xProperty.setValue(windowParams, xProperty.getValue(windowParams));
    150   }
    151 
    152   public void snapToBounds() {
    153     ensureSprings();
    154 
    155     moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX);
    156     moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams()));
    157   }
    158 
    159   @Override
    160   public boolean onTouch(View v, MotionEvent event) {
    161     float eventX = event.getRawX();
    162     float eventY = event.getRawY();
    163     switch (event.getActionMasked()) {
    164       case MotionEvent.ACTION_DOWN:
    165         firstX = eventX;
    166         firstY = eventY;
    167         velocityTracker = VelocityTracker.obtain();
    168         break;
    169       case MotionEvent.ACTION_MOVE:
    170         if (isMoving || hasExceededTouchSlop(event)) {
    171           if (!isMoving) {
    172             isMoving = true;
    173             bubble.onMoveStart();
    174           }
    175 
    176           ensureSprings();
    177 
    178           moveXAnimation.animateToFinalPosition(MathUtils.clamp(eventX, minX, maxX));
    179           moveYAnimation.animateToFinalPosition(MathUtils.clamp(eventY, minY, maxY));
    180         }
    181 
    182         velocityTracker.addMovement(event);
    183         break;
    184       case MotionEvent.ACTION_UP:
    185         if (isMoving) {
    186           ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
    187           velocityTracker.computeCurrentVelocity(
    188               1000, viewConfiguration.getScaledMaximumFlingVelocity());
    189           float xVelocity = velocityTracker.getXVelocity();
    190           float yVelocity = velocityTracker.getYVelocity();
    191           boolean isFling = isFling(xVelocity, yVelocity);
    192 
    193           if (isFling) {
    194             Point target =
    195                 findTarget(
    196                     xVelocity,
    197                     yVelocity,
    198                     (int) xProperty.getValue(bubble.getWindowParams()),
    199                     (int) yProperty.getValue(bubble.getWindowParams()));
    200 
    201             moveXAnimation.animateToFinalPosition(target.x);
    202             moveYAnimation.animateToFinalPosition(target.y);
    203           } else {
    204             snapX();
    205           }
    206           isMoving = false;
    207           bubble.onMoveFinish();
    208         } else {
    209           v.performClick();
    210           bubble.primaryButtonClick();
    211         }
    212         break;
    213     }
    214     return true;
    215   }
    216 
    217   private void ensureSprings() {
    218     if (moveXAnimation == null) {
    219       moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
    220       moveXAnimation.setSpring(new SpringForce());
    221       moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
    222     }
    223 
    224     if (moveYAnimation == null) {
    225       moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty);
    226       moveYAnimation.setSpring(new SpringForce());
    227       moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
    228     }
    229   }
    230 
    231   private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) {
    232     if (scroller == null) {
    233       scroller = new Scroller(context);
    234       scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
    235     }
    236 
    237     // Find where a fling would end vertically
    238     scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY);
    239     int targetY = scroller.getFinalY();
    240     scroller.abortAnimation();
    241 
    242     // If the x component of the velocity is above the minimum fling velocity, use velocity to
    243     // determine edge. Otherwise use its starting position
    244     boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX);
    245     return new Point(pullRight ? maxX : minX, targetY);
    246   }
    247 
    248   private boolean isFling(float xVelocity, float yVelocity) {
    249     int minFlingVelocity =
    250         ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
    251     return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity;
    252   }
    253 
    254   private boolean isOnRightHalf(float currentX) {
    255     return currentX > (minX + maxX) / 2;
    256   }
    257 
    258   private void snapX() {
    259     // Check if x value is closer to min or max
    260     boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams()));
    261     moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX);
    262   }
    263 
    264   private boolean relativeToRight(LayoutParams windowParams) {
    265     return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
    266   }
    267 
    268   private boolean hasExceededTouchSlop(MotionEvent event) {
    269     return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY)
    270         > touchSlopSquared;
    271   }
    272 
    273   private float getMagnitudeSquared(float deltaX, float deltaY) {
    274     return deltaX * deltaX + deltaY * deltaY;
    275   }
    276 }
    277