1 /* 2 * Copyright (C) 2015 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.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Rect; 22 import android.graphics.drawable.Drawable; 23 import android.support.v7.widget.LinearLayoutManager; 24 import android.support.v7.widget.RecyclerView.State; 25 import android.util.AttributeSet; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.Toast; 30 31 import com.android.launcher3.BaseContainerView; 32 import com.android.launcher3.CellLayout; 33 import com.android.launcher3.DeleteDropTarget; 34 import com.android.launcher3.DragSource; 35 import com.android.launcher3.DropTarget.DragObject; 36 import com.android.launcher3.dragndrop.DragOptions; 37 import com.android.launcher3.folder.Folder; 38 import com.android.launcher3.IconCache; 39 import com.android.launcher3.ItemInfo; 40 import com.android.launcher3.Launcher; 41 import com.android.launcher3.LauncherAppState; 42 import com.android.launcher3.PendingAddItemInfo; 43 import com.android.launcher3.R; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.WidgetPreviewLoader; 46 import com.android.launcher3.Workspace; 47 import com.android.launcher3.dragndrop.DragController; 48 import com.android.launcher3.model.WidgetsModel; 49 import com.android.launcher3.userevent.nano.LauncherLogProto; 50 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 51 import com.android.launcher3.util.Thunk; 52 import com.android.launcher3.util.TransformingTouchDelegate; 53 54 /** 55 * The widgets list view container. 56 */ 57 public class WidgetsContainerView extends BaseContainerView 58 implements View.OnLongClickListener, View.OnClickListener, DragSource { 59 private static final String TAG = "WidgetsContainerView"; 60 private static final boolean LOGD = false; 61 62 /* Global instances that are used inside this container. */ 63 @Thunk Launcher mLauncher; 64 private DragController mDragController; 65 private IconCache mIconCache; 66 67 private final Rect mTmpBgPaddingRect = new Rect(); 68 69 /* Recycler view related member variables */ 70 private WidgetsRecyclerView mRecyclerView; 71 private WidgetsListAdapter mAdapter; 72 private TransformingTouchDelegate mRecyclerViewTouchDelegate; 73 74 /* Touch handling related member variables. */ 75 private Toast mWidgetInstructionToast; 76 77 /* Rendering related. */ 78 private WidgetPreviewLoader mWidgetPreviewLoader; 79 WidgetsContainerView(Context context)80 public WidgetsContainerView(Context context) { 81 this(context, null); 82 } 83 WidgetsContainerView(Context context, AttributeSet attrs)84 public WidgetsContainerView(Context context, AttributeSet attrs) { 85 this(context, attrs, 0); 86 } 87 WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr)88 public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 89 super(context, attrs, defStyleAttr); 90 mLauncher = Launcher.getLauncher(context); 91 mDragController = mLauncher.getDragController(); 92 mAdapter = new WidgetsListAdapter(this, this, context); 93 mIconCache = (LauncherAppState.getInstance()).getIconCache(); 94 if (LOGD) { 95 Log.d(TAG, "WidgetsContainerView constructor"); 96 } 97 } 98 99 @Override onLayout(boolean changed, int left, int top, int right, int bottom)100 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 101 super.onLayout(changed, left, top, right, bottom); 102 getRevealView().getBackground().getPadding(mTmpBgPaddingRect); 103 mRecyclerViewTouchDelegate.setBounds( 104 mRecyclerView.getLeft() - mTmpBgPaddingRect.left, 105 mRecyclerView.getTop() - mTmpBgPaddingRect.top, 106 mRecyclerView.getRight() + mTmpBgPaddingRect.right, 107 mRecyclerView.getBottom() + mTmpBgPaddingRect.bottom); 108 } 109 110 @Override onFinishInflate()111 protected void onFinishInflate() { 112 super.onFinishInflate(); 113 mRecyclerView = (WidgetsRecyclerView) getContentView().findViewById(R.id.widgets_list_view); 114 mRecyclerView.setAdapter(mAdapter); 115 mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 116 mRecyclerViewTouchDelegate = new TransformingTouchDelegate(mRecyclerView); 117 } 118 119 @Override onAttachedToWindow()120 protected void onAttachedToWindow() { 121 super.onAttachedToWindow(); 122 ((View) mRecyclerView.getParent()).setTouchDelegate(mRecyclerViewTouchDelegate); 123 } 124 125 // 126 // Returns views used for launcher transitions. 127 // 128 scrollToTop()129 public void scrollToTop() { 130 mRecyclerView.scrollToPosition(0); 131 } 132 133 // 134 // Touch related handling. 135 // 136 137 @Override onClick(View v)138 public void onClick(View v) { 139 // When we have exited widget tray or are in transition, disregard clicks 140 if (!mLauncher.isWidgetsViewVisible() 141 || mLauncher.getWorkspace().isSwitchingState() 142 || !(v instanceof WidgetCell)) return; 143 144 // Let the user know that they have to long press to add a widget 145 if (mWidgetInstructionToast != null) { 146 mWidgetInstructionToast.cancel(); 147 } 148 149 CharSequence msg = Utilities.wrapForTts( 150 getContext().getText(R.string.long_press_widget_to_add), 151 getContext().getString(R.string.long_accessible_way_to_add)); 152 mWidgetInstructionToast = Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT); 153 mWidgetInstructionToast.show(); 154 } 155 156 @Override onLongClick(View v)157 public boolean onLongClick(View v) { 158 if (LOGD) { 159 Log.d(TAG, String.format("onLonglick [v=%s]", v)); 160 } 161 // Return early if this is not initiated from a touch 162 if (!v.isInTouchMode()) return false; 163 // When we have exited all apps or are in transition, disregard long clicks 164 if (!mLauncher.isWidgetsViewVisible() || 165 mLauncher.getWorkspace().isSwitchingState()) return false; 166 // Return if global dragging is not enabled 167 if (!mLauncher.isDraggingEnabled()) return false; 168 169 boolean status = beginDragging(v); 170 if (status && v.getTag() instanceof PendingAddWidgetInfo) { 171 WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v); 172 boolean preloadStatus = hostLoader.preloadWidget(); 173 if (LOGD) { 174 Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus)); 175 } 176 mLauncher.getDragController().addDragListener(hostLoader); 177 } 178 return status; 179 } 180 beginDragging(View v)181 private boolean beginDragging(View v) { 182 if (v instanceof WidgetCell) { 183 if (!beginDraggingWidget((WidgetCell) v)) { 184 return false; 185 } 186 } else { 187 Log.e(TAG, "Unexpected dragging view: " + v); 188 } 189 190 // We don't enter spring-loaded mode if the drag has been cancelled 191 if (mLauncher.getDragController().isDragging()) { 192 // Go into spring loaded mode (must happen before we startDrag()) 193 mLauncher.enterSpringLoadedDragMode(); 194 } 195 196 return true; 197 } 198 beginDraggingWidget(WidgetCell v)199 private boolean beginDraggingWidget(WidgetCell v) { 200 // Get the widget preview as the drag representation 201 WidgetImageView image = (WidgetImageView) v.findViewById(R.id.widget_preview); 202 PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); 203 204 // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and 205 // we abort the drag. 206 if (image.getBitmap() == null) { 207 return false; 208 } 209 210 // Compose the drag image 211 Bitmap preview; 212 final float scale; 213 final Rect bounds = image.getBitmapBounds(); 214 215 if (createItemInfo instanceof PendingAddWidgetInfo) { 216 // This can happen in some weird cases involving multi-touch. We can't start dragging 217 // the widget if this is null, so we break out. 218 219 PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo; 220 int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true); 221 222 Bitmap icon = image.getBitmap(); 223 float minScale = 1.25f; 224 int maxWidth = Math.min((int) (icon.getWidth() * minScale), size[0]); 225 226 int[] previewSizeBeforeScale = new int[1]; 227 preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher, 228 createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale); 229 230 if (previewSizeBeforeScale[0] < icon.getWidth()) { 231 // The icon has extra padding around it. 232 int padding = (icon.getWidth() - previewSizeBeforeScale[0]) / 2; 233 if (icon.getWidth() > image.getWidth()) { 234 padding = padding * image.getWidth() / icon.getWidth(); 235 } 236 237 bounds.left += padding; 238 bounds.right -= padding; 239 } 240 scale = bounds.width() / (float) preview.getWidth(); 241 } else { 242 PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); 243 Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo); 244 preview = Utilities.createIconBitmap(icon, mLauncher); 245 createItemInfo.spanX = createItemInfo.spanY = 1; 246 scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / preview.getWidth(); 247 } 248 249 // Since we are not going through the workspace for starting the drag, set drag related 250 // information on the workspace before starting the drag. 251 mLauncher.getWorkspace().prepareDragWithProvider( 252 new PendingItemPreviewProvider(v, createItemInfo, preview)); 253 254 // Start the drag 255 mDragController.startDrag(image, preview, this, createItemInfo, 256 bounds, scale, new DragOptions()); 257 return true; 258 } 259 260 // 261 // Drag related handling methods that implement {@link DragSource} interface. 262 // 263 264 @Override supportsFlingToDelete()265 public boolean supportsFlingToDelete() { 266 return true; 267 } 268 269 @Override supportsAppInfoDropTarget()270 public boolean supportsAppInfoDropTarget() { 271 return true; 272 } 273 274 /* 275 * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the 276 * {@link DeleteDropTarget} to be invisible.) 277 */ 278 @Override supportsDeleteDropTarget()279 public boolean supportsDeleteDropTarget() { 280 return false; 281 } 282 283 @Override getIntrinsicIconScaleFactor()284 public float getIntrinsicIconScaleFactor() { 285 return 0; 286 } 287 288 @Override onFlingToDeleteCompleted()289 public void onFlingToDeleteCompleted() { 290 // We just dismiss the drag when we fling, so cleanup here 291 mLauncher.exitSpringLoadedDragModeDelayed(true, 292 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); 293 mLauncher.unlockScreenOrientation(false); 294 } 295 296 @Override onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success)297 public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, 298 boolean success) { 299 if (LOGD) { 300 Log.d(TAG, "onDropCompleted"); 301 } 302 if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && 303 !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { 304 // Exit spring loaded mode if we have not successfully dropped or have not handled the 305 // drop in Workspace 306 mLauncher.exitSpringLoadedDragModeDelayed(true, 307 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); 308 } 309 mLauncher.unlockScreenOrientation(false); 310 311 // Display an error message if the drag failed due to there not being enough space on the 312 // target layout we were dropping on. 313 if (!success) { 314 boolean showOutOfSpaceMessage = false; 315 if (target instanceof Workspace) { 316 int currentScreen = mLauncher.getCurrentWorkspaceScreen(); 317 Workspace workspace = (Workspace) target; 318 CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); 319 ItemInfo itemInfo = d.dragInfo; 320 if (layout != null) { 321 showOutOfSpaceMessage = 322 !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); 323 } 324 } 325 if (showOutOfSpaceMessage) { 326 mLauncher.showOutOfSpaceMessage(false); 327 } 328 d.deferDragViewCleanupPostAnimation = false; 329 } 330 } 331 332 /** 333 * Initialize the widget data model. 334 */ addWidgets(WidgetsModel model)335 public void addWidgets(WidgetsModel model) { 336 mRecyclerView.setWidgets(model); 337 mAdapter.setWidgetsModel(model); 338 mAdapter.notifyDataSetChanged(); 339 340 View loader = getContentView().findViewById(R.id.loader); 341 if (loader != null) { 342 ((ViewGroup) getContentView()).removeView(loader); 343 } 344 } 345 isEmpty()346 public boolean isEmpty() { 347 return mAdapter.getItemCount() == 0; 348 } 349 getWidgetPreviewLoader()350 private WidgetPreviewLoader getWidgetPreviewLoader() { 351 if (mWidgetPreviewLoader == null) { 352 mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); 353 } 354 return mWidgetPreviewLoader; 355 } 356 357 @Override fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent)358 public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) { 359 targetParent.containerType = LauncherLogProto.WIDGETS; 360 } 361 }