1 /* 2 * Copyright (C) 2018 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.car.notification; 18 19 import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.annotation.IntDef; 24 import android.content.Context; 25 import android.os.Build; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.ViewPropertyAnimator; 29 30 import com.android.car.notification.template.CarNotificationBaseViewHolder; 31 32 import java.lang.annotation.Retention; 33 34 /** A general animation tool kit to dismiss {@link CarNotificationBaseViewHolder} */ 35 class DismissAnimationHelper { 36 private static final String TAG = "CarDismissHelper"; 37 private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG; 38 /** 39 * The weight of how much swipe distance plays on the alpha value of the view. 40 * A weight of 1F will make the view completely transparent if the swipe distance is larger 41 * than the view width. 42 */ 43 private static final float SWIPE_DISTANCE_WEIGHT_ON_ALPHA = 0.9F; 44 private final DismissCallback mCallBacks; 45 46 /** 47 * The direction of motion. 48 * <ol> 49 * <li> LEFT means swiping to the left. 50 * <li> RIGHT means swiping to the right. 51 * </ol> 52 */ 53 @Retention(SOURCE) 54 @IntDef({Direction.LEFT, Direction.RIGHT}) 55 public @interface Direction { 56 int LEFT = 1; 57 int RIGHT = 2; 58 } 59 60 /** 61 * The percentage of the view holder's width a non-dismissible view holder is allow to translate 62 * during a swipe gesture. As gesture's delta x distance grows the view holder should translate 63 * asymptotically to this amount. 64 */ 65 private final float mMaxPercentageOfWidthWithResistance; 66 67 /** 68 * The callback indicating the supplied view has been dismissed. 69 */ 70 interface DismissCallback { 71 72 /** 73 * Called after animation ends and the view is considered dismissed. 74 */ onDismiss(CarNotificationBaseViewHolder viewHolder)75 void onDismiss(CarNotificationBaseViewHolder viewHolder); 76 } 77 DismissAnimationHelper(Context context, DismissCallback callbacks)78 DismissAnimationHelper(Context context, DismissCallback callbacks) { 79 mCallBacks = callbacks; 80 81 mMaxPercentageOfWidthWithResistance = 82 context.getResources().getFloat(R.dimen.max_percentage_of_width_with_resistance); 83 } 84 85 /** Animate the dismissal of the given item. The velocityX is assumed to be 0. */ animateDismiss(CarNotificationBaseViewHolder viewHolder, @Direction int swipeDirection)86 void animateDismiss(CarNotificationBaseViewHolder viewHolder, 87 @Direction int swipeDirection) { 88 animateDismiss(viewHolder, swipeDirection, 0f); 89 } 90 91 /** Animate the dismissal of the given item. */ animateDismiss( CarNotificationBaseViewHolder viewHolder, @Direction int swipeDirection, float velocityX)92 void animateDismiss( 93 CarNotificationBaseViewHolder viewHolder, 94 @Direction int swipeDirection, 95 float velocityX) { 96 if (DEBUG) { 97 Log.d(TAG, "animateDismiss direction=" + swipeDirection + " velocityX=" + velocityX); 98 } 99 100 viewHolder.setIsAnimating(true); 101 102 int viewWidth = viewHolder.itemView.getWidth(); 103 ViewPropertyAnimator viewPropertyAnimator = viewHolder.itemView.animate() 104 .translationX(swipeDirection == Direction.RIGHT ? viewWidth : -viewWidth) 105 .alpha(0); 106 107 new Handler().postDelayed(() -> { 108 viewHolder.setIsAnimating(false); 109 mCallBacks.onDismiss(viewHolder); 110 }, viewPropertyAnimator.getDuration()); 111 viewPropertyAnimator.start(); 112 } 113 114 /** Animate the restore back of the given item back to it's initial state. */ animateRestore(CarNotificationBaseViewHolder viewHolder, float velocityX)115 void animateRestore(CarNotificationBaseViewHolder viewHolder, float velocityX) { 116 if (DEBUG) { 117 Log.d(TAG, "animateRestore velocityX=" + velocityX); 118 } 119 viewHolder.setIsAnimating(true); 120 121 viewHolder.itemView.animate() 122 .translationX(0) 123 .alpha(1) 124 .setListener(new AnimatorListenerAdapter() { 125 @Override 126 public void onAnimationEnd(Animator animation) { 127 viewHolder.setIsAnimating(false); 128 } 129 }); 130 } 131 calculateAlphaValue(CarNotificationBaseViewHolder viewHolder, float translateX)132 float calculateAlphaValue(CarNotificationBaseViewHolder viewHolder, float translateX) { 133 if (!viewHolder.isDismissible() || translateX == 0) { 134 return 1F; 135 } 136 137 int width = viewHolder.itemView.getWidth(); 138 return SWIPE_DISTANCE_WEIGHT_ON_ALPHA * (1 - Math.min(Math.abs(translateX / width), 1)) 139 + (1 - SWIPE_DISTANCE_WEIGHT_ON_ALPHA); 140 } 141 calculateTranslateDistance(CarNotificationBaseViewHolder viewHolder, float moveDeltaX)142 float calculateTranslateDistance(CarNotificationBaseViewHolder viewHolder, float moveDeltaX) { 143 // If we can dismiss then translate the same distance the touch event moved and if delta 144 // x is 0 just return 0. 145 if (viewHolder.isDismissible() || moveDeltaX == 0) { 146 return moveDeltaX; 147 } 148 149 // Calculate possible drag resistance. 150 int swipeDirection = moveDeltaX > 0 ? Direction.RIGHT : Direction.LEFT; 151 152 int width = viewHolder.itemView.getWidth(); 153 float maxSwipeDistanceWithResistance = mMaxPercentageOfWidthWithResistance * width; 154 if (Math.abs(moveDeltaX) >= width) { 155 // If deltaX is too large, constrain to 156 // maxScrollDistanceWithResistance. 157 return (swipeDirection == Direction.RIGHT) 158 ? maxSwipeDistanceWithResistance 159 : -maxSwipeDistanceWithResistance; 160 } else { 161 // Otherwise, just attenuate deltaX. 162 return maxSwipeDistanceWithResistance 163 * (float) Math.sin((moveDeltaX / width) * (Math.PI / 2)); 164 } 165 } 166 }