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