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