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