| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.widget; |
| |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.graphics.Bitmap; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.support.v7.widget.LinearLayoutManager; |
| import android.support.v7.widget.RecyclerView; |
| import android.support.v7.widget.RecyclerView.State; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.View; |
| import android.widget.ImageView; |
| import android.widget.Toast; |
| import com.android.launcher3.BaseContainerView; |
| import com.android.launcher3.CellLayout; |
| import com.android.launcher3.DeleteDropTarget; |
| import com.android.launcher3.DragController; |
| import com.android.launcher3.DragSource; |
| import com.android.launcher3.DropTarget.DragObject; |
| import com.android.launcher3.FastBitmapDrawable; |
| import com.android.launcher3.Folder; |
| import com.android.launcher3.IconCache; |
| import com.android.launcher3.Insettable; |
| import com.android.launcher3.ItemInfo; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherAppState; |
| import com.android.launcher3.PendingAddItemInfo; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.WidgetPreviewLoader; |
| import com.android.launcher3.Workspace; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * The widgets list view container. |
| */ |
| public class WidgetsContainerView extends BaseContainerView |
| implements View.OnLongClickListener, View.OnClickListener, DragSource{ |
| |
| private static final String TAG = "WidgetsContainerView"; |
| private static final boolean DEBUG = false; |
| |
| private static final int SPRING_MODE_DELAY_MS = 150; |
| |
| /* Coefficient multiplied to the screen height for preloading widgets. */ |
| private static final int PRELOAD_SCREEN_HEIGHT_MULTIPLE = 1; |
| |
| /* Global instances that are used inside this container. */ |
| private Launcher mLauncher; |
| private DragController mDragController; |
| private IconCache mIconCache; |
| |
| /* Data model for the widget */ |
| private WidgetsModel mWidgets; |
| |
| /* Recycler view related member variables */ |
| private RecyclerView mView; |
| private WidgetsListAdapter mAdapter; |
| |
| /* Touch handling related member variables. */ |
| private Toast mWidgetInstructionToast; |
| |
| /* Rendering related. */ |
| private WidgetPreviewLoader mWidgetPreviewLoader; |
| private WidgetHostViewLoader mWidgetHostViewLoader; |
| |
| private Rect mPadding = new Rect(); |
| |
| public WidgetsContainerView(Context context) { |
| this(context, null); |
| } |
| |
| public WidgetsContainerView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| mLauncher = (Launcher) context; |
| mDragController = mLauncher.getDragController(); |
| mWidgetHostViewLoader = new WidgetHostViewLoader(mLauncher); |
| mAdapter = new WidgetsListAdapter(context, this, this, mLauncher); |
| mWidgets = new WidgetsModel(context, mAdapter); |
| mAdapter.setWidgetsModel(mWidgets); |
| mIconCache = (LauncherAppState.getInstance()).getIconCache(); |
| |
| if (DEBUG) { |
| Log.d(TAG, "WidgetsContainerView constructor"); |
| } |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| if (DEBUG) { |
| Log.d(TAG, String.format("onFinishInflate [widgets size=%d]", |
| mWidgets.getPackageSize())); |
| } |
| mView = (RecyclerView) findViewById(R.id.widgets_list_view); |
| mView.setAdapter(mAdapter); |
| |
| // This extends the layout space so that preloading happen for the {@link RecyclerView} |
| mView.setLayoutManager(new LinearLayoutManager(getContext()) { |
| @Override |
| protected int getExtraLayoutSpace(State state) { |
| return super.getExtraLayoutSpace(state) |
| + WidgetsContainerView.this.getHeight() * PRELOAD_SCREEN_HEIGHT_MULTIPLE; |
| } |
| }); |
| mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), |
| getPaddingBottom()); |
| onUpdatePaddings(); |
| } |
| |
| // |
| // Returns views used for launcher transitions. |
| // |
| |
| public View getContentView() { |
| return findViewById(R.id.widgets_content); |
| } |
| |
| public View getRevealView() { |
| // TODO(hyunyoungs): temporarily use apps view transition. |
| return findViewById(R.id.widgets_reveal_view); |
| } |
| |
| public void scrollToTop() { |
| mView.scrollToPosition(0); |
| if (DEBUG) { |
| Log.d(TAG, String.format("scrollToTop, [widgets size=%d]", |
| mWidgets.getPackageSize())); |
| } |
| } |
| |
| // |
| // Touch related handling. |
| // |
| |
| @Override |
| public void onClick(View v) { |
| // When we have exited widget tray or are in transition, disregard clicks |
| if (!mLauncher.isWidgetsViewVisible() |
| || mLauncher.getWorkspace().isSwitchingState() |
| || !(v instanceof WidgetCell)) return; |
| |
| // Let the user know that they have to long press to add a widget |
| if (mWidgetInstructionToast != null) { |
| mWidgetInstructionToast.cancel(); |
| } |
| mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, |
| Toast.LENGTH_SHORT); |
| mWidgetInstructionToast.show(); |
| } |
| |
| @Override |
| public boolean onLongClick(View v) { |
| if (DEBUG) { |
| Log.d(TAG, String.format("onLonglick [v=%s]", v)); |
| } |
| // Return early if this is not initiated from a touch |
| if (!v.isInTouchMode()) return false; |
| // When we have exited all apps or are in transition, disregard long clicks |
| if (!mLauncher.isWidgetsViewVisible() || |
| mLauncher.getWorkspace().isSwitchingState()) return false; |
| // Return if global dragging is not enabled |
| Log.d(TAG, String.format("onLonglick dragging enabled?.", v)); |
| if (!mLauncher.isDraggingEnabled()) return false; |
| |
| boolean status = beginDragging(v); |
| if (status) { |
| mWidgetHostViewLoader.load(v); |
| } |
| return status; |
| } |
| |
| private boolean beginDragging(View v) { |
| if (v instanceof WidgetCell) { |
| if (!beginDraggingWidget((WidgetCell) v)) { |
| return false; |
| } |
| } else { |
| Log.e(TAG, "Unexpected dragging view: " + v); |
| } |
| |
| // We delay entering spring-loaded mode slightly to make sure the UI |
| // thread is free of any work. |
| postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| // We don't enter spring-loaded mode if the drag has been cancelled |
| if (mLauncher.getDragController().isDragging()) { |
| // Go into spring loaded mode (must happen before we startDrag()) |
| mLauncher.enterSpringLoadedDragMode(); |
| } |
| } |
| }, SPRING_MODE_DELAY_MS); |
| |
| return true; |
| } |
| |
| private boolean beginDraggingWidget(WidgetCell v) { |
| // Get the widget preview as the drag representation |
| ImageView image = (ImageView) v.findViewById(R.id.widget_preview); |
| PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); |
| |
| // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and |
| // we abort the drag. |
| if (image.getDrawable() == null) { |
| return false; |
| } |
| |
| // Compose the drag image |
| Bitmap preview; |
| Bitmap outline; |
| float scale = 1f; |
| Point previewPadding = null; |
| |
| if (createItemInfo instanceof PendingAddWidgetInfo) { |
| // This can happen in some weird cases involving multi-touch. We can't start dragging |
| // the widget if this is null, so we break out. |
| |
| PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo; |
| int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true); |
| |
| FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); |
| float minScale = 1.25f; |
| int maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); |
| |
| int[] previewSizeBeforeScale = new int[1]; |
| preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info, |
| maxWidth, null, previewSizeBeforeScale); |
| // Compare the size of the drag preview to the preview in the AppsCustomize tray |
| int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0], |
| v.getActualItemWidth()); |
| scale = previewWidthInAppsCustomize / (float) preview.getWidth(); |
| |
| // The bitmap in the AppsCustomize tray is always the the same size, so there |
| // might be extra pixels around the preview itself - this accounts for that |
| if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) { |
| int padding = |
| (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2; |
| previewPadding = new Point(padding, 0); |
| } |
| } else { |
| PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); |
| Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo); |
| preview = Utilities.createIconBitmap(icon, mLauncher); |
| createItemInfo.spanX = createItemInfo.spanY = 1; |
| } |
| |
| // Don't clip alpha values for the drag outline if we're using the default widget preview |
| boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && |
| (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); |
| |
| // Save the preview for the outline generation, then dim the preview |
| outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), |
| false); |
| |
| // Start the drag |
| mLauncher.lockScreenOrientation(); |
| mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha); |
| mDragController.startDrag(image, preview, this, createItemInfo, |
| DragController.DRAG_ACTION_COPY, previewPadding, scale); |
| outline.recycle(); |
| preview.recycle(); |
| return true; |
| } |
| |
| // |
| // Drag related handling methods that implement {@link DragSource} interface. |
| // |
| |
| @Override |
| public boolean supportsFlingToDelete() { |
| return false; |
| } |
| |
| @Override |
| public boolean supportsAppInfoDropTarget() { |
| return true; |
| } |
| |
| /* |
| * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the |
| * {@link DeleteDropTarget} to be invisible.) |
| */ |
| @Override |
| public boolean supportsDeleteDropTarget() { |
| return false; |
| } |
| |
| @Override |
| public float getIntrinsicIconScaleFactor() { |
| return 0; |
| } |
| |
| @Override |
| public void onFlingToDeleteCompleted() { |
| // We just dismiss the drag when we fling, so cleanup here |
| mLauncher.exitSpringLoadedDragModeDelayed(true, |
| Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); |
| mLauncher.unlockScreenOrientation(false); |
| } |
| |
| @Override |
| public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, |
| boolean success) { |
| if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && |
| !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { |
| // Exit spring loaded mode if we have not successfully dropped or have not handled the |
| // drop in Workspace |
| mLauncher.exitSpringLoadedDragModeDelayed(true, |
| Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); |
| } |
| mLauncher.unlockScreenOrientation(false); |
| |
| // Display an error message if the drag failed due to there not being enough space on the |
| // target layout we were dropping on. |
| if (!success) { |
| boolean showOutOfSpaceMessage = false; |
| if (target instanceof Workspace) { |
| int currentScreen = mLauncher.getCurrentWorkspaceScreen(); |
| Workspace workspace = (Workspace) target; |
| CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); |
| ItemInfo itemInfo = (ItemInfo) d.dragInfo; |
| if (layout != null) { |
| layout.calculateSpans(itemInfo); |
| showOutOfSpaceMessage = |
| !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); |
| } |
| } |
| if (showOutOfSpaceMessage) { |
| mLauncher.showOutOfSpaceMessage(false); |
| } |
| d.deferDragViewCleanupPostAnimation = false; |
| } |
| //TODO(hyunyoungs): if drop fails, this call cleans up correctly. |
| // However, in rare corner case where drop succeeds but doesn't end up using the widget |
| // id created by the loader, this finish will leave dangling widget id. |
| mWidgetHostViewLoader.finish(success); |
| } |
| |
| // |
| // Container rendering related. |
| // |
| |
| @Override |
| protected void onUpdatePaddings() { |
| if (mFixedBounds.isEmpty()) { |
| // If there are no fixed bounds, then use the default padding and insets |
| setPadding(mPadding.left + mInsets.left, mPadding.top + mInsets.top, |
| mPadding.right + mInsets.right, mPadding.bottom + mInsets.bottom); |
| } else { |
| // If there are fixed bounds, then we update the padding to reflect the fixed bounds. |
| setPadding(mFixedBounds.left, mFixedBounds.top, getMeasuredWidth() - mFixedBounds.right, |
| mInsets.bottom); |
| } |
| } |
| |
| /** |
| * Initialize the widget data model. |
| */ |
| public void addWidgets(ArrayList<Object> widgetsShortcuts, PackageManager pm) { |
| mWidgets.addWidgetsAndShortcuts(widgetsShortcuts, pm); |
| } |
| |
| private WidgetPreviewLoader getWidgetPreviewLoader() { |
| if (mWidgetPreviewLoader == null) { |
| mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); |
| } |
| return mWidgetPreviewLoader; |
| } |
| } |