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.launcher3.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.content.Context; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.view.Gravity; 26 import android.view.LayoutInflater; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.animation.AnimationUtils; 31 import android.view.animation.Interpolator; 32 import android.widget.TextView; 33 34 import com.android.launcher3.AbstractFloatingView; 35 import com.android.launcher3.DropTarget; 36 import com.android.launcher3.Insettable; 37 import com.android.launcher3.ItemInfo; 38 import com.android.launcher3.Launcher; 39 import com.android.launcher3.LauncherAnimUtils; 40 import com.android.launcher3.LauncherAppState; 41 import com.android.launcher3.R; 42 import com.android.launcher3.Utilities; 43 import com.android.launcher3.touch.SwipeDetector; 44 import com.android.launcher3.anim.PropertyListBuilder; 45 import com.android.launcher3.dragndrop.DragController; 46 import com.android.launcher3.dragndrop.DragOptions; 47 import com.android.launcher3.graphics.GradientView; 48 import com.android.launcher3.model.WidgetItem; 49 import com.android.launcher3.userevent.nano.LauncherLogProto; 50 import com.android.launcher3.util.PackageUserKey; 51 import com.android.launcher3.util.SystemUiController; 52 import com.android.launcher3.util.Themes; 53 import com.android.launcher3.util.TouchController; 54 55 import java.util.List; 56 57 /** 58 * Bottom sheet for the "Widgets" system shortcut in the long-press popup. 59 */ 60 public class WidgetsBottomSheet extends AbstractFloatingView implements Insettable, TouchController, 61 SwipeDetector.Listener, View.OnClickListener, View.OnLongClickListener, 62 DragController.DragListener { 63 64 private int mTranslationYOpen; 65 private int mTranslationYClosed; 66 private float mTranslationYRange; 67 68 private Launcher mLauncher; 69 private ItemInfo mOriginalItemInfo; 70 private ObjectAnimator mOpenCloseAnimator; 71 private Interpolator mFastOutSlowInInterpolator; 72 private SwipeDetector.ScrollInterpolator mScrollInterpolator; 73 private Rect mInsets; 74 private SwipeDetector mSwipeDetector; 75 private GradientView mGradientBackground; 76 77 public WidgetsBottomSheet(Context context, AttributeSet attrs) { 78 this(context, attrs, 0); 79 } 80 81 public WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr) { 82 super(context, attrs, defStyleAttr); 83 setWillNotDraw(false); 84 mLauncher = Launcher.getLauncher(context); 85 mOpenCloseAnimator = LauncherAnimUtils.ofPropertyValuesHolder(this); 86 mFastOutSlowInInterpolator = 87 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 88 mScrollInterpolator = new SwipeDetector.ScrollInterpolator(); 89 mInsets = new Rect(); 90 mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL); 91 mGradientBackground = (GradientView) mLauncher.getLayoutInflater().inflate( 92 R.layout.gradient_bg, mLauncher.getDragLayer(), false); 93 } 94 95 @Override 96 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 97 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 98 mTranslationYOpen = 0; 99 mTranslationYClosed = getMeasuredHeight(); 100 mTranslationYRange = mTranslationYClosed - mTranslationYOpen; 101 } 102 103 public void populateAndShow(ItemInfo itemInfo) { 104 mOriginalItemInfo = itemInfo; 105 ((TextView) findViewById(R.id.title)).setText(getContext().getString( 106 R.string.widgets_bottom_sheet_title, mOriginalItemInfo.title)); 107 108 onWidgetsBound(); 109 110 mLauncher.getDragLayer().addView(mGradientBackground); 111 mGradientBackground.setVisibility(VISIBLE); 112 mLauncher.getDragLayer().addView(this); 113 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 114 setTranslationY(mTranslationYClosed); 115 mIsOpen = false; 116 open(true); 117 } 118 119 @Override 120 protected void onWidgetsBound() { 121 List<WidgetItem> widgets = mLauncher.getWidgetsForPackageUser(new PackageUserKey( 122 mOriginalItemInfo.getTargetComponent().getPackageName(), mOriginalItemInfo.user)); 123 124 ViewGroup widgetRow = (ViewGroup) findViewById(R.id.widgets); 125 ViewGroup widgetCells = (ViewGroup) widgetRow.findViewById(R.id.widgets_cell_list); 126 127 widgetCells.removeAllViews(); 128 129 for (int i = 0; i < widgets.size(); i++) { 130 WidgetCell widget = addItemCell(widgetCells); 131 widget.applyFromCellItem(widgets.get(i), LauncherAppState.getInstance(mLauncher) 132 .getWidgetCache()); 133 widget.ensurePreview(); 134 widget.setVisibility(View.VISIBLE); 135 if (i < widgets.size() - 1) { 136 addDivider(widgetCells); 137 } 138 } 139 140 if (widgets.size() == 1) { 141 // If there is only one widget, we want to center it instead of left-align. 142 WidgetsBottomSheet.LayoutParams params = (WidgetsBottomSheet.LayoutParams) 143 widgetRow.getLayoutParams(); 144 params.gravity = Gravity.CENTER_HORIZONTAL; 145 } else { 146 // Otherwise, add an empty view to the start as padding (but still scroll edge to edge). 147 View leftPaddingView = LayoutInflater.from(getContext()).inflate( 148 R.layout.widget_list_divider, widgetRow, false); 149 leftPaddingView.getLayoutParams().width = Utilities.pxFromDp( 150 16, getResources().getDisplayMetrics()); 151 widgetCells.addView(leftPaddingView, 0); 152 } 153 } 154 155 private void addDivider(ViewGroup parent) { 156 LayoutInflater.from(getContext()).inflate(R.layout.widget_list_divider, parent, true); 157 } 158 159 private WidgetCell addItemCell(ViewGroup parent) { 160 WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext()).inflate( 161 R.layout.widget_cell, parent, false); 162 163 widget.setOnClickListener(this); 164 widget.setOnLongClickListener(this); 165 widget.setAnimatePreview(false); 166 167 parent.addView(widget); 168 return widget; 169 } 170 171 @Override 172 public void onClick(View view) { 173 mLauncher.getWidgetsView().handleClick(); 174 } 175 176 @Override 177 public boolean onLongClick(View view) { 178 mLauncher.getDragController().addDragListener(this); 179 return mLauncher.getWidgetsView().handleLongClick(view); 180 } 181 182 private void open(boolean animate) { 183 if (mIsOpen || mOpenCloseAnimator.isRunning()) { 184 return; 185 } 186 mIsOpen = true; 187 boolean isSheetDark = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark); 188 mLauncher.getSystemUiController().updateUiState( 189 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 190 isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV); 191 if (animate) { 192 mOpenCloseAnimator.setValues(new PropertyListBuilder() 193 .translationY(mTranslationYOpen).build()); 194 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 195 @Override 196 public void onAnimationEnd(Animator animation) { 197 mSwipeDetector.finishedScrolling(); 198 } 199 }); 200 mOpenCloseAnimator.setInterpolator(mFastOutSlowInInterpolator); 201 mOpenCloseAnimator.start(); 202 } else { 203 setTranslationY(mTranslationYOpen); 204 } 205 } 206 207 @Override 208 protected void handleClose(boolean animate) { 209 if (!mIsOpen || mOpenCloseAnimator.isRunning()) { 210 return; 211 } 212 if (animate) { 213 mOpenCloseAnimator.setValues(new PropertyListBuilder() 214 .translationY(mTranslationYClosed).build()); 215 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 216 @Override 217 public void onAnimationEnd(Animator animation) { 218 mSwipeDetector.finishedScrolling(); 219 onCloseComplete(); 220 } 221 }); 222 mOpenCloseAnimator.setInterpolator(mSwipeDetector.isIdleState() 223 ? mFastOutSlowInInterpolator : mScrollInterpolator); 224 mOpenCloseAnimator.start(); 225 } else { 226 setTranslationY(mTranslationYClosed); 227 onCloseComplete(); 228 } 229 } 230 231 private void onCloseComplete() { 232 mIsOpen = false; 233 mLauncher.getDragLayer().removeView(mGradientBackground); 234 mLauncher.getDragLayer().removeView(WidgetsBottomSheet.this); 235 mLauncher.getSystemUiController().updateUiState( 236 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0); 237 } 238 239 @Override 240 protected boolean isOfType(@FloatingViewType int type) { 241 return (type & TYPE_WIDGETS_BOTTOM_SHEET) != 0; 242 } 243 244 @Override 245 public int getLogContainerType() { 246 return LauncherLogProto.ContainerType.WIDGETS; // TODO: be more specific 247 } 248 249 /** 250 * Returns a {@link WidgetsBottomSheet} which is already open or null 251 */ 252 public static WidgetsBottomSheet getOpen(Launcher launcher) { 253 return getOpenView(launcher, TYPE_WIDGETS_BOTTOM_SHEET); 254 } 255 256 @Override 257 public void setInsets(Rect insets) { 258 // Extend behind left, right, and bottom insets. 259 int leftInset = insets.left - mInsets.left; 260 int rightInset = insets.right - mInsets.right; 261 int bottomInset = insets.bottom - mInsets.bottom; 262 mInsets.set(insets); 263 setPadding(getPaddingLeft() + leftInset, getPaddingTop(), 264 getPaddingRight() + rightInset, getPaddingBottom() + bottomInset); 265 } 266 267 /* SwipeDetector.Listener */ 268 269 @Override 270 public void onDragStart(boolean start) { 271 } 272 273 @Override 274 public boolean onDrag(float displacement, float velocity) { 275 setTranslationY(Utilities.boundToRange(displacement, mTranslationYOpen, 276 mTranslationYClosed)); 277 return true; 278 } 279 280 @Override 281 public void setTranslationY(float translationY) { 282 super.setTranslationY(translationY); 283 if (mGradientBackground == null) return; 284 float p = (mTranslationYClosed - translationY) / mTranslationYRange; 285 boolean showScrim = p <= 0; 286 mGradientBackground.setProgress(p, showScrim); 287 } 288 289 @Override 290 public void onDragEnd(float velocity, boolean fling) { 291 if ((fling && velocity > 0) || getTranslationY() > (mTranslationYRange) / 2) { 292 mScrollInterpolator.setVelocityAtZero(velocity); 293 mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(velocity, 294 (mTranslationYClosed - getTranslationY()) / mTranslationYRange)); 295 close(true); 296 } else { 297 mIsOpen = false; 298 mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(velocity, 299 (getTranslationY() - mTranslationYOpen) / mTranslationYRange)); 300 open(true); 301 } 302 } 303 304 @Override 305 public boolean onControllerTouchEvent(MotionEvent ev) { 306 return mSwipeDetector.onTouchEvent(ev); 307 } 308 309 @Override 310 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 311 int directionsToDetectScroll = mSwipeDetector.isIdleState() ? 312 SwipeDetector.DIRECTION_NEGATIVE : 0; 313 mSwipeDetector.setDetectableScrollConditions( 314 directionsToDetectScroll, false); 315 mSwipeDetector.onTouchEvent(ev); 316 return mSwipeDetector.isDraggingOrSettling(); 317 } 318 319 /* DragListener */ 320 321 @Override 322 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 323 // A widget or custom shortcut was dragged. 324 close(true); 325 } 326 327 @Override 328 public void onDragEnd() { 329 } 330 } 331