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