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