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 package com.android.launcher3.views; 17 18 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 19 20 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.ObjectAnimator; 25 import android.animation.PropertyValuesHolder; 26 import android.content.Context; 27 import android.util.AttributeSet; 28 import android.util.Property; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.animation.Interpolator; 32 33 import com.android.launcher3.AbstractFloatingView; 34 import com.android.launcher3.Utilities; 35 import com.android.launcher3.anim.Interpolators; 36 import com.android.launcher3.touch.BaseSwipeDetector; 37 import com.android.launcher3.touch.SingleAxisSwipeDetector; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom. 44 * 45 * @param <T> Type of ActivityContext inflating this view. 46 */ 47 public abstract class AbstractSlideInView<T extends Context & ActivityContext> 48 extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener { 49 50 protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT = 51 new Property<AbstractSlideInView, Float>(Float.class, "translationShift") { 52 53 @Override 54 public Float get(AbstractSlideInView view) { 55 return view.mTranslationShift; 56 } 57 58 @Override 59 public void set(AbstractSlideInView view, Float value) { 60 view.setTranslationShift(value); 61 } 62 }; 63 protected static final float TRANSLATION_SHIFT_CLOSED = 1f; 64 protected static final float TRANSLATION_SHIFT_OPENED = 0f; 65 66 protected final T mActivityContext; 67 68 protected final SingleAxisSwipeDetector mSwipeDetector; 69 protected final ObjectAnimator mOpenCloseAnimator; 70 71 protected View mContent; 72 protected final View mColorScrim; 73 protected Interpolator mScrollInterpolator; 74 75 // range [0, 1], 0=> completely open, 1=> completely closed 76 protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED; 77 78 protected boolean mNoIntercept; 79 protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>(); 80 AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr)81 public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) { 82 super(context, attrs, defStyleAttr); 83 mActivityContext = ActivityContext.lookupContext(context); 84 85 mScrollInterpolator = Interpolators.SCROLL_CUBIC; 86 mSwipeDetector = new SingleAxisSwipeDetector(context, this, 87 SingleAxisSwipeDetector.VERTICAL); 88 89 mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this); 90 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 91 @Override 92 public void onAnimationEnd(Animator animation) { 93 mSwipeDetector.finishedScrolling(); 94 announceAccessibilityChanges(); 95 } 96 }); 97 int scrimColor = getScrimColor(context); 98 mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null; 99 } 100 attachToContainer()101 protected void attachToContainer() { 102 if (mColorScrim != null) { 103 getPopupContainer().addView(mColorScrim); 104 } 105 getPopupContainer().addView(this); 106 } 107 108 /** 109 * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added. 110 */ getScrimColor(Context context)111 protected int getScrimColor(Context context) { 112 return -1; 113 } 114 setTranslationShift(float translationShift)115 protected void setTranslationShift(float translationShift) { 116 mTranslationShift = translationShift; 117 mContent.setTranslationY(mTranslationShift * mContent.getHeight()); 118 if (mColorScrim != null) { 119 mColorScrim.setAlpha(1 - mTranslationShift); 120 } 121 } 122 123 @Override onControllerInterceptTouchEvent(MotionEvent ev)124 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 125 if (mNoIntercept) { 126 return false; 127 } 128 129 int directionsToDetectScroll = mSwipeDetector.isIdleState() 130 ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0; 131 mSwipeDetector.setDetectableScrollConditions( 132 directionsToDetectScroll, false); 133 mSwipeDetector.onTouchEvent(ev); 134 return mSwipeDetector.isDraggingOrSettling() 135 || !getPopupContainer().isEventOverView(mContent, ev); 136 } 137 138 @Override onControllerTouchEvent(MotionEvent ev)139 public boolean onControllerTouchEvent(MotionEvent ev) { 140 mSwipeDetector.onTouchEvent(ev); 141 if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState() 142 && !isOpeningAnimationRunning()) { 143 // If we got ACTION_UP without ever starting swipe, close the panel. 144 if (!getPopupContainer().isEventOverView(mContent, ev)) { 145 close(true); 146 } 147 } 148 return true; 149 } 150 isOpeningAnimationRunning()151 private boolean isOpeningAnimationRunning() { 152 return mIsOpen && mOpenCloseAnimator.isRunning(); 153 } 154 155 /* SingleAxisSwipeDetector.Listener */ 156 157 @Override onDragStart(boolean start, float startDisplacement)158 public void onDragStart(boolean start, float startDisplacement) { } 159 160 @Override onDrag(float displacement)161 public boolean onDrag(float displacement) { 162 float range = mContent.getHeight(); 163 displacement = Utilities.boundToRange(displacement, 0, range); 164 setTranslationShift(displacement / range); 165 return true; 166 } 167 168 @Override onDragEnd(float velocity)169 public void onDragEnd(float velocity) { 170 if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) { 171 mScrollInterpolator = scrollInterpolatorForVelocity(velocity); 172 mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration( 173 velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift)); 174 close(true); 175 } else { 176 mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat( 177 TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); 178 mOpenCloseAnimator.setDuration( 179 BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) 180 .setInterpolator(Interpolators.DEACCEL); 181 mOpenCloseAnimator.start(); 182 } 183 } 184 185 /** Registers an {@link OnCloseListener}. */ addOnCloseListener(OnCloseListener listener)186 public void addOnCloseListener(OnCloseListener listener) { 187 mOnCloseListeners.add(listener); 188 } 189 handleClose(boolean animate, long defaultDuration)190 protected void handleClose(boolean animate, long defaultDuration) { 191 if (!mIsOpen) { 192 return; 193 } 194 if (!animate) { 195 mOpenCloseAnimator.cancel(); 196 setTranslationShift(TRANSLATION_SHIFT_CLOSED); 197 onCloseComplete(); 198 return; 199 } 200 mOpenCloseAnimator.setValues( 201 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED)); 202 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 203 @Override 204 public void onAnimationEnd(Animator animation) { 205 onCloseComplete(); 206 } 207 }); 208 if (mSwipeDetector.isIdleState()) { 209 mOpenCloseAnimator 210 .setDuration(defaultDuration) 211 .setInterpolator(Interpolators.ACCEL); 212 } else { 213 mOpenCloseAnimator.setInterpolator(mScrollInterpolator); 214 } 215 mOpenCloseAnimator.start(); 216 } 217 onCloseComplete()218 protected void onCloseComplete() { 219 mIsOpen = false; 220 getPopupContainer().removeView(this); 221 if (mColorScrim != null) { 222 getPopupContainer().removeView(mColorScrim); 223 } 224 mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed); 225 } 226 getPopupContainer()227 protected BaseDragLayer getPopupContainer() { 228 return mActivityContext.getDragLayer(); 229 } 230 createColorScrim(Context context, int bgColor)231 protected View createColorScrim(Context context, int bgColor) { 232 View view = new View(context); 233 view.forceHasOverlappingRendering(false); 234 view.setBackgroundColor(bgColor); 235 236 BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT); 237 lp.ignoreInsets = true; 238 view.setLayoutParams(lp); 239 240 return view; 241 } 242 243 /** 244 * Interface to report that the {@link AbstractSlideInView} has closed. 245 */ 246 public interface OnCloseListener { 247 248 /** 249 * Called when {@link AbstractSlideInView} closes. 250 */ onSlideInViewClosed()251 void onSlideInViewClosed(); 252 } 253 } 254