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