diff options
Diffstat (limited to 'src/com/android/launcher3/widget')
9 files changed, 1380 insertions, 0 deletions
diff --git a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java new file mode 100644 index 000000000..a56985083 --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java @@ -0,0 +1,44 @@ +/* + * 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.ComponentName; +import android.content.pm.ActivityInfo; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; + +/** + * Meta data used for late binding of the short cuts. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddShortcutInfo extends PendingAddItemInfo { + + ActivityInfo activityInfo; + + public PendingAddShortcutInfo(ActivityInfo activityInfo) { + this.activityInfo = activityInfo; + componentName = new ComponentName(activityInfo.packageName, activityInfo.name); + itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + } + + @Override + public String toString() { + return String.format("PendingAddShortcutInfo package=%s, name=%s", + activityInfo.packageName, activityInfo.name); + } +} diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java new file mode 100644 index 000000000..758287af3 --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -0,0 +1,75 @@ +/* + * 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.appwidget.AppWidgetHostView; +import android.os.Bundle; +import android.os.Parcelable; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.compat.AppWidgetManagerCompat; + +/** + * Meta data used for late binding of {@link LauncherAppWidgetProviderInfo}. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddWidgetInfo extends PendingAddItemInfo { + public int minWidth; + public int minHeight; + public int minResizeWidth; + public int minResizeHeight; + public int previewImage; + public int icon; + public LauncherAppWidgetProviderInfo info; + public AppWidgetHostView boundWidget; + public Bundle bindOptions = null; + + public PendingAddWidgetInfo(Launcher launcher, LauncherAppWidgetProviderInfo i, Parcelable data) { + if (i.isCustomWidget) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } else { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + } + this.info = i; + user = AppWidgetManagerCompat.getInstance(launcher).getUser(i); + componentName = i.provider; + minWidth = i.minWidth; + minHeight = i.minHeight; + minResizeWidth = i.minResizeWidth; + minResizeHeight = i.minResizeHeight; + previewImage = i.previewImage; + icon = i.icon; + + spanX = i.getSpanX(launcher); + spanY = i.getSpanY(launcher); + minSpanX = i.getMinSpanX(launcher); + minSpanY = i.getMinSpanY(launcher); + } + + public boolean isCustomWidget() { + return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } + + @Override + public String toString() { + return String.format("PendingAddWidgetInfo package=%s, name=%s", + componentName.getPackageName(), componentName.getShortClassName()); + } +} diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java new file mode 100644 index 000000000..7496ea2ef --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -0,0 +1,230 @@ +/* + * 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.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewPropertyAnimator; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.StylusEventHelper; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.WidgetPreviewLoader.PreviewLoadRequest; +import com.android.launcher3.compat.AppWidgetManagerCompat; + +/** + * Represents the individual cell of the widget inside the widget tray. The preview is drawn + * horizontally centered, and scaled down if needed. + * + * This view does not support padding. Since the image is scaled down to fit the view, padding will + * further decrease the scaling factor. Drag-n-drop uses the view bounds for showing a smooth + * transition from the view to drag view, so when adding padding support, DnD would need to + * consider the appropriate scaling factor. + */ +public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { + + private static final String TAG = "WidgetCell"; + private static final boolean DEBUG = false; + + private static final int FADE_IN_DURATION_MS = 90; + + /** Widget cell width is calculated by multiplying this factor to grid cell width. */ + private static final float WIDTH_SCALE = 2.6f; + + /** Widget preview width is calculated by multiplying this factor to the widget cell width. */ + private static final float PREVIEW_SCALE = 0.8f; + + private int mPresetPreviewSize; + int cellSize; + + private WidgetImageView mWidgetImage; + private TextView mWidgetName; + private TextView mWidgetDims; + + private String mDimensionsFormatString; + private Object mInfo; + + private WidgetPreviewLoader mWidgetPreviewLoader; + private PreviewLoadRequest mActiveRequest; + private StylusEventHelper mStylusEventHelper; + + private Launcher mLauncher; + + public WidgetCell(Context context) { + this(context, null); + } + + public WidgetCell(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetCell(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final Resources r = context.getResources(); + mLauncher = (Launcher) context; + mStylusEventHelper = new StylusEventHelper(this); + + mDimensionsFormatString = r.getString(R.string.widget_dims_format); + setContainerWidth(); + setWillNotDraw(false); + setClipToPadding(false); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + } + + private void setContainerWidth() { + DeviceProfile profile = mLauncher.getDeviceProfile(); + cellSize = (int) (profile.cellWidthPx * WIDTH_SCALE); + mPresetPreviewSize = (int) (cellSize * PREVIEW_SCALE); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mWidgetImage = (WidgetImageView) findViewById(R.id.widget_preview); + mWidgetName = ((TextView) findViewById(R.id.widget_name)); + mWidgetDims = ((TextView) findViewById(R.id.widget_dims)); + } + + /** + * Called to clear the view and free attached resources. (e.g., {@link Bitmap} + */ + public void clear() { + if (DEBUG) { + Log.d(TAG, "reset called on:" + mWidgetName.getText()); + } + mWidgetImage.animate().cancel(); + mWidgetImage.setBitmap(null); + mWidgetName.setText(null); + mWidgetDims.setText(null); + + if (mActiveRequest != null) { + mActiveRequest.cleanup(); + mActiveRequest = null; + } + } + + /** + * Apply the widget provider info to the view. + */ + public void applyFromAppWidgetProviderInfo(LauncherAppWidgetProviderInfo info, + WidgetPreviewLoader loader) { + + InvariantDeviceProfile profile = + LauncherAppState.getInstance().getInvariantDeviceProfile(); + mInfo = info; + // TODO(hyunyoungs): setup a cache for these labels. + mWidgetName.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); + int hSpan = Math.min(info.getSpanX(mLauncher), profile.numColumns); + int vSpan = Math.min(info.getSpanY(mLauncher), profile.numRows); + mWidgetDims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); + mWidgetPreviewLoader = loader; + } + + /** + * Apply the resolve info to the view. + */ + public void applyFromResolveInfo( + PackageManager pm, ResolveInfo info, WidgetPreviewLoader loader) { + mInfo = info; + CharSequence label = info.loadLabel(pm); + mWidgetName.setText(label); + mWidgetDims.setText(String.format(mDimensionsFormatString, 1, 1)); + mWidgetPreviewLoader = loader; + } + + public int[] getPreviewSize() { + int[] maxSize = new int[2]; + + maxSize[0] = mPresetPreviewSize; + maxSize[1] = mPresetPreviewSize; + return maxSize; + } + + public void applyPreview(Bitmap bitmap) { + if (bitmap != null) { + mWidgetImage.setBitmap(bitmap); + mWidgetImage.setAlpha(0f); + ViewPropertyAnimator anim = mWidgetImage.animate(); + anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS); + } + } + + public void ensurePreview() { + if (mActiveRequest != null) { + return; + } + int[] size = getPreviewSize(); + if (DEBUG) { + Log.d(TAG, String.format("[tag=%s] ensurePreview (%d, %d):", + getTagToString(), size[0], size[1])); + } + mActiveRequest = mWidgetPreviewLoader.getPreview(mInfo, size[0], size[1], this); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + removeOnLayoutChangeListener(this); + ensurePreview(); + } + + public int getActualItemWidth() { + ItemInfo info = (ItemInfo) getTag(); + int[] size = getPreviewSize(); + int cellWidth = mLauncher.getDeviceProfile().cellWidthPx; + + return Math.min(size[0], info.spanX * cellWidth); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = super.onTouchEvent(ev); + if (mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + return true; + } + return handled; + } + + /** + * Helper method to get the string info of the tag. + */ + private String getTagToString() { + if (getTag() instanceof PendingAddWidgetInfo || + getTag() instanceof PendingAddShortcutInfo) { + return getTag().toString(); + } + return ""; + } +} diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java new file mode 100644 index 000000000..30b3d581a --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -0,0 +1,155 @@ +package com.android.launcher3.widget; + +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; + +import com.android.launcher3.AppWidgetResizeFrame; +import com.android.launcher3.DragController.DragListener; +import com.android.launcher3.DragLayer; +import com.android.launcher3.DragSource; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.util.Thunk; + +public class WidgetHostViewLoader implements DragListener { + + /* Runnables to handle inflation and binding. */ + @Thunk Runnable mInflateWidgetRunnable = null; + private Runnable mBindWidgetRunnable = null; + + // TODO: technically, this class should not have to know the existence of the launcher. + @Thunk Launcher mLauncher; + @Thunk Handler mHandler; + @Thunk final View mView; + @Thunk final PendingAddWidgetInfo mInfo; + + // Widget id generated for binding a widget host view or -1 for invalid id. The id is + // not is use as long as it is stored here and can be deleted safely. Once its used, this value + // to be set back to -1. + @Thunk int mWidgetLoadingId = -1; + + public WidgetHostViewLoader(Launcher launcher, View view) { + mLauncher = launcher; + mHandler = new Handler(); + mView = view; + mInfo = (PendingAddWidgetInfo) view.getTag(); + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { } + + @Override + public void onDragEnd() { + // Cleanup up preloading state. + mLauncher.getDragController().removeDragListener(this); + + mHandler.removeCallbacks(mBindWidgetRunnable); + mHandler.removeCallbacks(mInflateWidgetRunnable); + + // Cleanup widget id + if (mWidgetLoadingId != -1) { + mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); + mWidgetLoadingId = -1; + } + + // The widget was inflated and added to the DragLayer -- remove it. + if (mInfo.boundWidget != null) { + mLauncher.getDragLayer().removeView(mInfo.boundWidget); + mLauncher.getAppWidgetHost().deleteAppWidgetId(mInfo.boundWidget.getAppWidgetId()); + mInfo.boundWidget = null; + } + } + + /** + * Start preloading the widget. + */ + public boolean preloadWidget() { + final LauncherAppWidgetProviderInfo pInfo = mInfo.info; + + if (pInfo.isCustomWidget) { + return false; + } + final Bundle options = getDefaultOptionsForWidget(mLauncher, mInfo); + + // If there is a configuration activity, do not follow thru bound and inflate. + if (pInfo.configure != null) { + mInfo.bindOptions = options; + return false; + } + + mBindWidgetRunnable = new Runnable() { + @Override + public void run() { + mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); + if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, pInfo, options)) { + + // Widget id bound. Inflate the widget. + mHandler.post(mInflateWidgetRunnable); + } + } + }; + + mInflateWidgetRunnable = new Runnable() { + @Override + public void run() { + if (mWidgetLoadingId == -1) { + return; + } + AppWidgetHostView hostView = mLauncher.getAppWidgetHost().createView( + (Context) mLauncher, mWidgetLoadingId, pInfo); + mInfo.boundWidget = hostView; + + // We used up the widget Id in binding the above view. + mWidgetLoadingId = -1; + + hostView.setVisibility(View.INVISIBLE); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(mInfo, false); + // We want the first widget layout to be the correct size. This will be important + // for width size reporting to the AppWidgetManager. + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], + unScaledSize[1]); + lp.x = lp.y = 0; + lp.customPosition = true; + hostView.setLayoutParams(lp); + mLauncher.getDragLayer().addView(hostView); + mView.setTag(mInfo); + } + }; + + mHandler.post(mBindWidgetRunnable); + return true; + } + + public static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { + Bundle options = null; + Rect rect = new Rect(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, rect); + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, + info.componentName, null); + + float density = launcher.getResources().getDisplayMetrics().density; + int xPaddingDips = (int) ((padding.left + padding.right) / density); + int yPaddingDips = (int) ((padding.top + padding.bottom) / density); + + options = new Bundle(); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + rect.left - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + rect.top - yPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + rect.right - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + rect.bottom - yPaddingDips); + } + return options; + } +} diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java new file mode 100644 index 000000000..b0fbe1ed9 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetImageView.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 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.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +/** + * View that draws a bitmap horizontally centered. If the image width is greater than the view + * width, the image is scaled down appropriately. + */ +public class WidgetImageView extends View { + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private final RectF mDstRectF = new RectF(); + private Bitmap mBitmap; + + public WidgetImageView(Context context) { + super(context); + } + + public WidgetImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WidgetImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + invalidate(); + } + + public Bitmap getBitmap() { + return mBitmap; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap != null) { + updateDstRectF(); + canvas.drawBitmap(mBitmap, null, mDstRectF, mPaint); + } + } + + /** + * Prevents the inefficient alpha view rendering. + */ + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private void updateDstRectF() { + if (mBitmap.getWidth() > getWidth()) { + float scale = ((float) getWidth()) / mBitmap.getWidth(); + mDstRectF.set(0, 0, getWidth(), scale * mBitmap.getHeight()); + } else { + mDstRectF.set( + (getWidth() - mBitmap.getWidth()) * 0.5f, + 0, + (getWidth() + mBitmap.getWidth()) * 0.5f, + mBitmap.getHeight()); + } + } + + /** + * @return the bounds where the image was drawn. + */ + public Rect getBitmapBounds() { + updateDstRectF(); + Rect rect = new Rect(); + mDstRectF.round(rect); + return rect; + } +} diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java new file mode 100644 index 000000000..5afd7c493 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -0,0 +1,370 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView.State; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.android.launcher3.BaseContainerView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DragController; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.Folder; +import com.android.launcher3.IconCache; +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 com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.Thunk; + +/** + * 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; + + /* 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. */ + @Thunk Launcher mLauncher; + private DragController mDragController; + private IconCache mIconCache; + + /* Recycler view related member variables */ + private View mContent; + private WidgetsRecyclerView mView; + private WidgetsListAdapter mAdapter; + + /* Touch handling related member variables. */ + private Toast mWidgetInstructionToast; + + /* Rendering related. */ + private WidgetPreviewLoader mWidgetPreviewLoader; + + 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(); + mAdapter = new WidgetsListAdapter(context, this, this, mLauncher); + mIconCache = (LauncherAppState.getInstance()).getIconCache(); + if (DEBUG) { + Log.d(TAG, "WidgetsContainerView constructor"); + } + } + + @Override + protected void onFinishInflate() { + mContent = findViewById(R.id.content); + mView = (WidgetsRecyclerView) 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) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + return super.getExtraLayoutSpace(state) + + grid.availableHeightPx * PRELOAD_SCREEN_HEIGHT_MULTIPLE; + } + }); + mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), + getPaddingBottom()); + } + + // + // Returns views used for launcher transitions. + // + + public View getContentView() { + return mView; + } + + public View getRevealView() { + // TODO(hyunyoungs): temporarily use apps view transition. + return findViewById(R.id.widgets_reveal_view); + } + + public void scrollToTop() { + mView.scrollToPosition(0); + } + + // + // 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 && v.getTag() instanceof PendingAddWidgetInfo) { + WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v); + boolean preloadStatus = hostLoader.preloadWidget(); + if (DEBUG) { + Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus)); + } + mLauncher.getDragController().addDragListener(hostLoader); + } + 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 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(); + } + + return true; + } + + private boolean beginDraggingWidget(WidgetCell v) { + // Get the widget preview as the drag representation + WidgetImageView image = (WidgetImageView) 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.getBitmap() == null) { + return false; + } + + // Compose the drag image + Bitmap preview; + float scale = 1f; + final Rect bounds = image.getBitmapBounds(); + + 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); + + Bitmap icon = image.getBitmap(); + float minScale = 1.25f; + int maxWidth = Math.min((int) (icon.getWidth() * minScale), size[0]); + + int[] previewSizeBeforeScale = new int[1]; + preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher, + createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale); + + if (previewSizeBeforeScale[0] < icon.getWidth()) { + // The icon has extra padding around it. + int padding = (icon.getWidth() - previewSizeBeforeScale[0]) / 2; + if (icon.getWidth() > image.getWidth()) { + padding = padding * image.getWidth() / icon.getWidth(); + } + + bounds.left += padding; + bounds.right -= padding; + } + scale = bounds.width() / (float) preview.getWidth(); + } else { + PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); + Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo); + preview = Utilities.createIconBitmap(icon, mLauncher); + createItemInfo.spanX = createItemInfo.spanY = 1; + scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / preview.getWidth(); + } + + // 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)); + + // Start the drag + mLauncher.lockScreenOrientation(); + mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, preview, clipAlpha); + mDragController.startDrag(image, preview, this, createItemInfo, + bounds, DragController.DRAG_ACTION_COPY, scale); + + 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; + } + } + + // + // Container rendering related. + // + + @Override + protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { + // Apply the top-bottom padding to the content itself so that the launcher transition is + // clipped correctly + mContent.setPadding(0, padding.top, 0, padding.bottom); + + // TODO: Use quantum_panel_dark instead of quantum_panel_shape_dark. + InsetDrawable background = new InsetDrawable( + getResources().getDrawable(R.drawable.quantum_panel_shape_dark), padding.left, 0, + padding.right, 0); + Rect bgPadding = new Rect(); + background.getPadding(bgPadding); + mView.setBackground(background); + getRevealView().setBackground(background.getConstantState().newDrawable()); + mView.updateBackgroundPadding(bgPadding); + } + + /** + * Initialize the widget data model. + */ + public void addWidgets(WidgetsModel model) { + mView.setWidgets(model); + mAdapter.setWidgetsModel(model); + mAdapter.notifyDataSetChanged(); + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java new file mode 100644 index 000000000..d2ea25230 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -0,0 +1,215 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.LinearLayout; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.model.WidgetsModel; + +import java.util.List; + +/** + * List view adapter for the widget tray. + * + * <p>Memory vs. Performance: + * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling + * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is + * only a single type of view. + */ +public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { + + private static final String TAG = "WidgetsListAdapter"; + private static final boolean DEBUG = false; + + private Launcher mLauncher; + private LayoutInflater mLayoutInflater; + + private WidgetsModel mWidgetsModel; + private WidgetPreviewLoader mWidgetPreviewLoader; + + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + + private static final int PRESET_INDENT_SIZE_TABLET = 56; + private int mIndent = 0; + + public WidgetsListAdapter(Context context, + View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener, + Launcher launcher) { + mLayoutInflater = LayoutInflater.from(context); + + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mLauncher = launcher; + + setContainerHeight(); + } + + public void setWidgetsModel(WidgetsModel w) { + mWidgetsModel = w; + } + + @Override + public int getItemCount() { + return mWidgetsModel.getPackageSize(); + } + + @Override + public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) { + List<Object> infoList = mWidgetsModel.getSortedWidgets(pos); + + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + if (DEBUG) { + Log.d(TAG, String.format( + "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]", + pos, infoList.size(), row.getChildCount())); + } + + // Add more views. + // if there are too many, hide them. + int diff = infoList.size() - row.getChildCount(); + + if (diff > 0) { + for (int i = 0; i < diff; i++) { + WidgetCell widget = (WidgetCell) mLayoutInflater.inflate( + R.layout.widget_cell, row, false); + + // set up touch. + widget.setOnClickListener(mIconClickListener); + widget.setOnLongClickListener(mIconLongClickListener); + LayoutParams lp = widget.getLayoutParams(); + lp.height = widget.cellSize; + lp.width = widget.cellSize; + widget.setLayoutParams(lp); + + row.addView(widget); + } + } else if (diff < 0) { + for (int i=infoList.size() ; i < row.getChildCount(); i++) { + row.getChildAt(i).setVisibility(View.GONE); + } + } + + // Bind the views in the application info section. + PackageItemInfo infoOut = mWidgetsModel.getPackageItemInfo(pos); + BubbleTextView tv = ((BubbleTextView) holder.getContent().findViewById(R.id.section)); + tv.applyFromPackageItemInfo(infoOut); + + // Bind the view in the widget horizontal tray region. + if (getWidgetPreviewLoader() == null) { + return; + } + for (int i=0; i < infoList.size(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + if (infoList.get(i) instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) infoList.get(i); + PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(mLauncher, info, null); + widget.setTag(pawi); + widget.applyFromAppWidgetProviderInfo(info, mWidgetPreviewLoader); + } else if (infoList.get(i) instanceof ResolveInfo) { + ResolveInfo info = (ResolveInfo) infoList.get(i); + PendingAddShortcutInfo pasi = new PendingAddShortcutInfo(info.activityInfo); + widget.setTag(pasi); + widget.applyFromResolveInfo(mLauncher.getPackageManager(), info, mWidgetPreviewLoader); + } + widget.ensurePreview(); + widget.setVisibility(View.VISIBLE); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (DEBUG) { + Log.v(TAG, "\nonCreateViewHolder"); + } + + ViewGroup container = (ViewGroup) mLayoutInflater.inflate( + R.layout.widgets_list_row_view, parent, false); + LinearLayout cellList = (LinearLayout) container.findViewById(R.id.widgets_cell_list); + + // if the end padding is 0, then container view (horizontal scroll view) doesn't respect + // the end of the linear layout width + the start padding and doesn't allow scrolling. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + cellList.setPaddingRelative(mIndent, 0, 1, 0); + } else { + cellList.setPadding(mIndent, 0, 1, 0); + } + + return new WidgetsRowViewHolder(container); + } + + @Override + public void onViewRecycled(WidgetsRowViewHolder holder) { + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + + for (int i = 0; i < row.getChildCount(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + widget.clear(); + } + } + + public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) { + // If child views are animating, then the RecyclerView may choose not to recycle the view, + // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue + // recycling this view, and take care in onViewRecycled() to cancel any existing + // animations. + return true; + } + + @Override + public long getItemId(int pos) { + return pos; + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } + + private void setContainerHeight() { + Resources r = mLauncher.getResources(); + DeviceProfile profile = mLauncher.getDeviceProfile(); + if (profile.isLargeTablet || profile.isTablet) { + mIndent = Utilities.pxFromDp(PRESET_INDENT_SIZE_TABLET, r.getDisplayMetrics()); + } + } +} diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java new file mode 100644 index 000000000..61e63cdb7 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -0,0 +1,158 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Color; +import android.support.v7.widget.LinearLayoutManager; +import android.util.AttributeSet; +import android.view.View; +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.R; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.model.WidgetsModel; + +/** + * The widgets recycler view. + */ +public class WidgetsRecyclerView extends BaseRecyclerView { + + private static final String TAG = "WidgetsRecyclerView"; + private WidgetsModel mWidgets; + private ScrollPositionState mScrollPosState = new ScrollPositionState(); + + public WidgetsRecyclerView(Context context) { + this(context, null); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + // API 21 and below only support 3 parameter ctor. + super(context, attrs, defStyleAttr); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + this(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + public int getFastScrollerTrackColor(int defaultTrackColor) { + return Color.WHITE; + } + + public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { + return getResources().getColor(R.color.widgets_view_fastscroll_thumb_inactive_color); + } + + /** + * Sets the widget model in this view, used to determine the fast scroll position. + */ + public void setWidgets(WidgetsModel widgets) { + mWidgets = widgets; + } + + /** + * We need to override the draw to ensure that we don't draw the overscroll effect beyond the + * background bounds. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + super.dispatchDraw(canvas); + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + */ + @Override + public String scrollToPositionAtProgress(float touchFraction) { + int rowCount = mWidgets.getPackageSize(); + if (rowCount == 0) { + return ""; + } + + // Stop the scroller if it is scrolling + stopScroll(); + + getCurScrollState(mScrollPosState); + float pos = rowCount * touchFraction; + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); + + int posInt = (int) ((touchFraction == 1)? pos -1 : pos); + PackageItemInfo p = mWidgets.getPackageItemInfo(posInt); + return p.titleSectionName; + } + + /** + * Updates the bounds for the scrollbar. + */ + @Override + public void onUpdateScrollbar() { + int rowCount = mWidgets.getPackageSize(); + + // Skip early if, there are no items. + if (rowCount == 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Skip early if, there no child laid out in the container. + getCurScrollState(mScrollPosState); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + } + + /** + * Returns the current scroll state. + */ + private void getCurScrollState(ScrollPositionState stateOut) { + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + + int rowCount = mWidgets.getPackageSize(); + + // Return early if there are no items + if (rowCount == 0) { + return; + } + View child = getChildAt(0); + int position = getChildPosition(child); + + stateOut.rowIndex = position; + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight(); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java new file mode 100644 index 000000000..249559ab9 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java @@ -0,0 +1,36 @@ +/* + * 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.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +public class WidgetsRowViewHolder extends ViewHolder { + + ViewGroup mContent; + + public WidgetsRowViewHolder(ViewGroup v) { + super(v); + mContent = v; + } + + ViewGroup getContent() { + return mContent; + } +} |