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 static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY; 20 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 21 22 import android.animation.PropertyValuesHolder; 23 import android.content.Context; 24 import android.graphics.Rect; 25 import android.util.AttributeSet; 26 import android.util.IntProperty; 27 import android.util.Pair; 28 import android.view.Gravity; 29 import android.view.LayoutInflater; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.animation.Interpolator; 34 import android.widget.ScrollView; 35 import android.widget.TableLayout; 36 import android.widget.TableRow; 37 import android.widget.TextView; 38 39 import com.android.launcher3.DeviceProfile; 40 import com.android.launcher3.LauncherAppState; 41 import com.android.launcher3.R; 42 import com.android.launcher3.anim.PendingAnimation; 43 import com.android.launcher3.model.WidgetItem; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.util.PackageUserKey; 46 import com.android.launcher3.widget.util.WidgetsTableUtils; 47 48 import java.util.List; 49 50 /** 51 * Bottom sheet for the "Widgets" system shortcut in the long-press popup. 52 */ 53 public class WidgetsBottomSheet extends BaseWidgetSheet { 54 private static final String TAG = "WidgetsBottomSheet"; 55 56 private static final IntProperty<View> PADDING_BOTTOM = 57 new IntProperty<View>("paddingBottom") { 58 @Override 59 public void setValue(View view, int paddingBottom) { 60 view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), 61 view.getPaddingRight(), paddingBottom); 62 } 63 64 @Override 65 public Integer get(View view) { 66 return view.getPaddingBottom(); 67 } 68 }; 69 70 private static final int DEFAULT_CLOSE_DURATION = 200; 71 private static final long EDUCATION_TIP_DELAY_MS = 300; 72 73 private ItemInfo mOriginalItemInfo; 74 private final int mMaxTableHeight; 75 private int mMaxHorizontalSpan = 4; 76 77 private final OnLayoutChangeListener mLayoutChangeListenerToShowTips = 78 new OnLayoutChangeListener() { 79 @Override 80 public void onLayoutChange(View v, int left, int top, int right, int bottom, 81 int oldLeft, int oldTop, int oldRight, int oldBottom) { 82 if (hasSeenEducationTip()) { 83 removeOnLayoutChangeListener(this); 84 return; 85 } 86 // Widgets are loaded asynchronously, We are adding a delay because we only want 87 // to show the tip when the widget preview has finished loading and rendering in 88 // this view. 89 removeCallbacks(mShowEducationTipTask); 90 postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS); 91 } 92 }; 93 94 private final Runnable mShowEducationTipTask = () -> { 95 if (hasSeenEducationTip()) { 96 removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 97 return; 98 } 99 View viewForTip = ((ViewGroup) ((TableLayout) findViewById(R.id.widgets_table)) 100 .getChildAt(0)).getChildAt(0); 101 if (showEducationTipOnViewIfPossible(viewForTip) != null) { 102 removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 103 } 104 }; 105 WidgetsBottomSheet(Context context, AttributeSet attrs)106 public WidgetsBottomSheet(Context context, AttributeSet attrs) { 107 this(context, attrs, 0); 108 } 109 WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr)110 public WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr) { 111 super(context, attrs, defStyleAttr); 112 setWillNotDraw(false); 113 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 114 // Set the max table height to 2 / 3 of the grid height so that the bottom picker won't 115 // take over the entire view vertically. 116 mMaxTableHeight = deviceProfile.inv.numRows * 2 / 3 * deviceProfile.cellHeightPx; 117 if (!hasSeenEducationTip()) { 118 addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 119 } 120 } 121 122 @Override onFinishInflate()123 protected void onFinishInflate() { 124 super.onFinishInflate(); 125 mContent = findViewById(R.id.widgets_bottom_sheet); 126 } 127 128 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)129 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 130 doMeasure(widthMeasureSpec, heightMeasureSpec); 131 if (updateMaxSpansPerRow()) { 132 doMeasure(widthMeasureSpec, heightMeasureSpec); 133 } 134 } 135 136 /** Returns {@code true} if the max spans have been updated. */ updateMaxSpansPerRow()137 private boolean updateMaxSpansPerRow() { 138 if (getMeasuredWidth() == 0) return false; 139 140 int paddingPx = 2 * getResources().getDimensionPixelOffset( 141 R.dimen.widget_cell_horizontal_padding); 142 int maxHorizontalSpan = findViewById(R.id.widgets_table).getMeasuredWidth() 143 / (mActivityContext.getDeviceProfile().cellWidthPx + paddingPx); 144 if (mMaxHorizontalSpan != maxHorizontalSpan) { 145 // Ensure the table layout is showing widgets in the right column after measure. 146 mMaxHorizontalSpan = maxHorizontalSpan; 147 onWidgetsBound(); 148 return true; 149 } 150 return false; 151 } 152 153 @Override onLayout(boolean changed, int l, int t, int r, int b)154 protected void onLayout(boolean changed, int l, int t, int r, int b) { 155 int width = r - l; 156 int height = b - t; 157 158 // Content is laid out as center bottom aligned. 159 int contentWidth = mContent.getMeasuredWidth(); 160 int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left; 161 mContent.layout(contentLeft, height - mContent.getMeasuredHeight(), 162 contentLeft + contentWidth, height); 163 164 setTranslationShift(mTranslationShift); 165 166 // Ensure the scroll view height is not larger than mMaxTableHeight, which is a value 167 // smaller than the entire screen height. 168 ScrollView widgetsTableScrollView = findViewById(R.id.widgets_table_scroll_view); 169 if (widgetsTableScrollView.getMeasuredHeight() > mMaxTableHeight) { 170 ViewGroup.LayoutParams layoutParams = widgetsTableScrollView.getLayoutParams(); 171 layoutParams.height = mMaxTableHeight; 172 widgetsTableScrollView.setLayoutParams(layoutParams); 173 findViewById(R.id.collapse_handle).setVisibility(VISIBLE); 174 } 175 } 176 populateAndShow(ItemInfo itemInfo)177 public void populateAndShow(ItemInfo itemInfo) { 178 mOriginalItemInfo = itemInfo; 179 ((TextView) findViewById(R.id.title)).setText(mOriginalItemInfo.title); 180 181 onWidgetsBound(); 182 attachToContainer(); 183 mIsOpen = false; 184 animateOpen(); 185 } 186 187 @Override onWidgetsBound()188 public void onWidgetsBound() { 189 List<WidgetItem> widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser( 190 new PackageUserKey( 191 mOriginalItemInfo.getTargetComponent().getPackageName(), 192 mOriginalItemInfo.user)); 193 194 TableLayout widgetsTable = findViewById(R.id.widgets_table); 195 widgetsTable.removeAllViews(); 196 197 WidgetsTableUtils.groupWidgetItemsIntoTable(widgets, mMaxHorizontalSpan).forEach(row -> { 198 TableRow tableRow = new TableRow(getContext()); 199 tableRow.setGravity(Gravity.TOP); 200 row.forEach(widgetItem -> { 201 WidgetCell widget = addItemCell(tableRow); 202 widget.setPreviewSize(widgetItem); 203 widget.applyFromCellItem(widgetItem, LauncherAppState.getInstance(mActivityContext) 204 .getWidgetCache()); 205 widget.ensurePreview(); 206 widget.setVisibility(View.VISIBLE); 207 }); 208 widgetsTable.addView(tableRow); 209 }); 210 } 211 212 @Override onControllerInterceptTouchEvent(MotionEvent ev)213 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 214 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 215 mNoIntercept = false; 216 ScrollView scrollView = findViewById(R.id.widgets_table_scroll_view); 217 if (getPopupContainer().isEventOverView(scrollView, ev) 218 && scrollView.getScrollY() > 0) { 219 mNoIntercept = true; 220 } 221 } 222 return super.onControllerInterceptTouchEvent(ev); 223 } 224 addItemCell(ViewGroup parent)225 protected WidgetCell addItemCell(ViewGroup parent) { 226 WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext()) 227 .inflate(R.layout.widget_cell, parent, false); 228 229 View previewContainer = widget.findViewById(R.id.widget_preview_container); 230 previewContainer.setOnClickListener(this); 231 previewContainer.setOnLongClickListener(this); 232 widget.setAnimatePreview(false); 233 widget.setSourceContainer(CONTAINER_BOTTOM_WIDGETS_TRAY); 234 235 parent.addView(widget); 236 return widget; 237 } 238 animateOpen()239 private void animateOpen() { 240 if (mIsOpen || mOpenCloseAnimator.isRunning()) { 241 return; 242 } 243 mIsOpen = true; 244 setupNavBarColor(); 245 mOpenCloseAnimator.setValues( 246 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); 247 mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN); 248 mOpenCloseAnimator.start(); 249 } 250 251 @Override handleClose(boolean animate)252 protected void handleClose(boolean animate) { 253 handleClose(animate, DEFAULT_CLOSE_DURATION); 254 } 255 256 @Override isOfType(@loatingViewType int type)257 protected boolean isOfType(@FloatingViewType int type) { 258 return (type & TYPE_WIDGETS_BOTTOM_SHEET) != 0; 259 } 260 261 @Override setInsets(Rect insets)262 public void setInsets(Rect insets) { 263 super.setInsets(insets); 264 265 mContent.setPadding(mContent.getPaddingStart(), 266 mContent.getPaddingTop(), mContent.getPaddingEnd(), insets.bottom); 267 if (insets.bottom > 0) { 268 setupNavBarColor(); 269 } else { 270 clearNavBarColor(); 271 } 272 } 273 274 @Override getAccessibilityTarget()275 protected Pair<View, String> getAccessibilityTarget() { 276 return Pair.create(findViewById(R.id.title), getContext().getString( 277 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed)); 278 } 279 280 @Override addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)281 public void addHintCloseAnim( 282 float distanceToMove, Interpolator interpolator, PendingAnimation target) { 283 target.setInt(this, PADDING_BOTTOM, (int) (distanceToMove + mInsets.bottom), interpolator); 284 } 285 } 286