| package com.android.launcher3; |
| |
| import static android.appwidget.AppWidgetHostView.getDefaultPaddingForWidget; |
| |
| import static com.android.launcher3.CellLayout.SPRING_LOADED_PROGRESS; |
| import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT; |
| import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_COMPLETED; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_STARTED; |
| import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X; |
| import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.PropertyValuesHolder; |
| import android.appwidget.AppWidgetProviderInfo; |
| import android.content.Context; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.GradientDrawable; |
| import android.util.AttributeSet; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.Px; |
| |
| import com.android.launcher3.accessibility.DragViewStateAnnouncer; |
| import com.android.launcher3.celllayout.CellLayoutLayoutParams; |
| import com.android.launcher3.celllayout.CellPosMapper.CellPos; |
| import com.android.launcher3.dragndrop.DragLayer; |
| import com.android.launcher3.keyboard.ViewGroupFocusHelper; |
| import com.android.launcher3.logging.InstanceId; |
| import com.android.launcher3.logging.InstanceIdSequence; |
| import com.android.launcher3.model.data.ItemInfo; |
| import com.android.launcher3.util.PendingRequestArgs; |
| import com.android.launcher3.views.ArrowTipView; |
| import com.android.launcher3.widget.LauncherAppWidgetHostView; |
| import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; |
| import com.android.launcher3.widget.util.WidgetSizes; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class AppWidgetResizeFrame extends AbstractFloatingView implements View.OnKeyListener { |
| private static final int SNAP_DURATION = 150; |
| private static final float DIMMED_HANDLE_ALPHA = 0f; |
| private static final float RESIZE_THRESHOLD = 0.66f; |
| |
| private static final String KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN = |
| "launcher.reconfigurable_widget_education_tip_seen"; |
| private static final Rect sTmpRect = new Rect(); |
| private static final Rect sTmpRect2 = new Rect(); |
| |
| private static final int HANDLE_COUNT = 4; |
| private static final int INDEX_LEFT = 0; |
| private static final int INDEX_TOP = 1; |
| private static final int INDEX_RIGHT = 2; |
| private static final int INDEX_BOTTOM = 3; |
| private static final float MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE = 0.5f; |
| |
| private final Launcher mLauncher; |
| private final DragViewStateAnnouncer mStateAnnouncer; |
| private final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper; |
| |
| private final View[] mDragHandles = new View[HANDLE_COUNT]; |
| private final List<Rect> mSystemGestureExclusionRects = new ArrayList<>(HANDLE_COUNT); |
| |
| private LauncherAppWidgetHostView mWidgetView; |
| private CellLayout mCellLayout; |
| private DragLayer mDragLayer; |
| private ImageButton mReconfigureButton; |
| |
| private Rect mWidgetPadding; |
| |
| private final int mBackgroundPadding; |
| private final int mTouchTargetWidth; |
| |
| private final int[] mDirectionVector = new int[2]; |
| private final int[] mLastDirectionVector = new int[2]; |
| |
| private final IntRange mTempRange1 = new IntRange(); |
| private final IntRange mTempRange2 = new IntRange(); |
| |
| private final IntRange mDeltaXRange = new IntRange(); |
| private final IntRange mBaselineX = new IntRange(); |
| |
| private final IntRange mDeltaYRange = new IntRange(); |
| private final IntRange mBaselineY = new IntRange(); |
| |
| private final InstanceId logInstanceId = new InstanceIdSequence().newInstanceId(); |
| |
| private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper; |
| |
| /** |
| * In the two panel UI, it is not possible to resize a widget to cross its host |
| * {@link CellLayout}'s sibling. When this happens, we gradually reduce the opacity of the |
| * sibling {@link CellLayout} from 1f to |
| * {@link #MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE}. |
| */ |
| private final float mDragAcrossTwoPanelOpacityMargin; |
| |
| private boolean mLeftBorderActive; |
| private boolean mRightBorderActive; |
| private boolean mTopBorderActive; |
| private boolean mBottomBorderActive; |
| |
| private boolean mHorizontalResizeActive; |
| private boolean mVerticalResizeActive; |
| |
| private int mRunningHInc; |
| private int mRunningVInc; |
| private int mMinHSpan; |
| private int mMinVSpan; |
| private int mMaxHSpan; |
| private int mMaxVSpan; |
| private int mDeltaX; |
| private int mDeltaY; |
| private int mDeltaXAddOn; |
| private int mDeltaYAddOn; |
| |
| private int mTopTouchRegionAdjustment = 0; |
| private int mBottomTouchRegionAdjustment = 0; |
| |
| private int mXDown, mYDown; |
| |
| public AppWidgetResizeFrame(Context context) { |
| this(context, null); |
| } |
| |
| public AppWidgetResizeFrame(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| mLauncher = Launcher.getLauncher(context); |
| mStateAnnouncer = DragViewStateAnnouncer.createFor(this); |
| |
| mBackgroundPadding = getResources() |
| .getDimensionPixelSize(R.dimen.resize_frame_background_padding); |
| mTouchTargetWidth = 2 * mBackgroundPadding; |
| mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this); |
| |
| for (int i = 0; i < HANDLE_COUNT; i++) { |
| mSystemGestureExclusionRects.add(new Rect()); |
| } |
| |
| mDragAcrossTwoPanelOpacityMargin = mLauncher.getResources().getDimensionPixelSize( |
| R.dimen.resize_frame_invalid_drag_across_two_panel_opacity_margin); |
| mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer()); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| |
| mDragHandles[INDEX_LEFT] = findViewById(R.id.widget_resize_left_handle); |
| mDragHandles[INDEX_TOP] = findViewById(R.id.widget_resize_top_handle); |
| mDragHandles[INDEX_RIGHT] = findViewById(R.id.widget_resize_right_handle); |
| mDragHandles[INDEX_BOTTOM] = findViewById(R.id.widget_resize_bottom_handle); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| super.onLayout(changed, l, t, r, b); |
| if (Utilities.ATLEAST_Q) { |
| for (int i = 0; i < HANDLE_COUNT; i++) { |
| View dragHandle = mDragHandles[i]; |
| mSystemGestureExclusionRects.get(i).set(dragHandle.getLeft(), dragHandle.getTop(), |
| dragHandle.getRight(), dragHandle.getBottom()); |
| } |
| setSystemGestureExclusionRects(mSystemGestureExclusionRects); |
| } |
| } |
| |
| public static void showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout) { |
| Launcher launcher = Launcher.getLauncher(cellLayout.getContext()); |
| AbstractFloatingView.closeAllOpenViews(launcher); |
| |
| DragLayer dl = launcher.getDragLayer(); |
| AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater() |
| .inflate(R.layout.app_widget_resize_frame, dl, false); |
| if (widget.hasEnforcedCornerRadius()) { |
| float enforcedCornerRadius = widget.getEnforcedCornerRadius(); |
| ImageView imageView = frame.findViewById(R.id.widget_resize_frame); |
| Drawable d = imageView.getDrawable(); |
| if (d instanceof GradientDrawable) { |
| GradientDrawable gd = (GradientDrawable) d.mutate(); |
| gd.setCornerRadius(enforcedCornerRadius); |
| } |
| } |
| frame.setupForWidget(widget, cellLayout, dl); |
| ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true; |
| |
| dl.addView(frame); |
| frame.mIsOpen = true; |
| frame.post(() -> frame.snapToWidget(false)); |
| } |
| |
| private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, |
| DragLayer dragLayer) { |
| mCellLayout = cellLayout; |
| mWidgetView = widgetView; |
| LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) |
| widgetView.getAppWidgetInfo(); |
| mDragLayer = dragLayer; |
| |
| mMinHSpan = info.minSpanX; |
| mMinVSpan = info.minSpanY; |
| mMaxHSpan = info.maxSpanX; |
| mMaxVSpan = info.maxSpanY; |
| |
| mWidgetPadding = getDefaultPaddingForWidget(getContext(), |
| widgetView.getAppWidgetInfo().provider, null); |
| |
| // Only show resize handles for the directions in which resizing is possible. |
| InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext()); |
| mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0 |
| && mMinVSpan < idp.numRows && mMaxVSpan > 1 |
| && mMinVSpan < mMaxVSpan; |
| if (!mVerticalResizeActive) { |
| mDragHandles[INDEX_TOP].setVisibility(GONE); |
| mDragHandles[INDEX_BOTTOM].setVisibility(GONE); |
| } |
| mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0 |
| && mMinHSpan < idp.numColumns && mMaxHSpan > 1 |
| && mMinHSpan < mMaxHSpan; |
| if (!mHorizontalResizeActive) { |
| mDragHandles[INDEX_LEFT].setVisibility(GONE); |
| mDragHandles[INDEX_RIGHT].setVisibility(GONE); |
| } |
| |
| mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button); |
| if (info.isReconfigurable()) { |
| mReconfigureButton.setVisibility(VISIBLE); |
| mReconfigureButton.setOnClickListener(view -> { |
| mLauncher.setWaitingForResult( |
| PendingRequestArgs.forWidgetInfo( |
| mWidgetView.getAppWidgetId(), |
| // Widget add handler is null since we're reconfiguring an existing |
| // widget. |
| /* widgetHandler= */ null, |
| (ItemInfo) mWidgetView.getTag())); |
| mLauncher |
| .getAppWidgetHolder() |
| .startConfigActivity( |
| mLauncher, |
| mWidgetView.getAppWidgetId(), |
| Launcher.REQUEST_RECONFIGURE_APPWIDGET); |
| }); |
| if (!hasSeenReconfigurableWidgetEducationTip()) { |
| post(() -> { |
| if (showReconfigurableWidgetEducationTip() != null) { |
| mLauncher.getSharedPrefs().edit() |
| .putBoolean(KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, |
| true).apply(); |
| } |
| }); |
| } |
| } |
| |
| CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams(); |
| ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag(); |
| CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo); |
| lp.setCellX(presenterPos.cellX); |
| lp.setTmpCellX(presenterPos.cellX); |
| lp.setCellY(presenterPos.cellY); |
| lp.setTmpCellY(presenterPos.cellY); |
| lp.cellHSpan = widgetInfo.spanX; |
| lp.cellVSpan = widgetInfo.spanY; |
| lp.isLockedToGrid = true; |
| |
| // When we create the resize frame, we first mark all cells as unoccupied. The appropriate |
| // cells (same if not resized, or different) will be marked as occupied when the resize |
| // frame is dismissed. |
| mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); |
| |
| mLauncher.getStatsLogManager() |
| .logger() |
| .withInstanceId(logInstanceId) |
| .withItemInfo(widgetInfo) |
| .log(LAUNCHER_WIDGET_RESIZE_STARTED); |
| |
| setOnKeyListener(this); |
| } |
| |
| public boolean beginResizeIfPointInRegion(int x, int y) { |
| mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive; |
| mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive; |
| mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) |
| && mVerticalResizeActive; |
| mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) |
| && mVerticalResizeActive; |
| |
| boolean anyBordersActive = mLeftBorderActive || mRightBorderActive |
| || mTopBorderActive || mBottomBorderActive; |
| |
| if (anyBordersActive) { |
| mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); |
| mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); |
| mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); |
| mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); |
| } |
| |
| if (mLeftBorderActive) { |
| mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth); |
| } else if (mRightBorderActive) { |
| mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight()); |
| } else { |
| mDeltaXRange.set(0, 0); |
| } |
| mBaselineX.set(getLeft(), getRight()); |
| |
| if (mTopBorderActive) { |
| mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth); |
| } else if (mBottomBorderActive) { |
| mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom()); |
| } else { |
| mDeltaYRange.set(0, 0); |
| } |
| mBaselineY.set(getTop(), getBottom()); |
| |
| return anyBordersActive; |
| } |
| |
| /** |
| * Based on the deltas, we resize the frame. |
| */ |
| public void visualizeResizeForDelta(int deltaX, int deltaY) { |
| mDeltaX = mDeltaXRange.clamp(deltaX); |
| mDeltaY = mDeltaYRange.clamp(deltaY); |
| |
| DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); |
| mDeltaX = mDeltaXRange.clamp(deltaX); |
| mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1); |
| lp.x = mTempRange1.start; |
| lp.width = mTempRange1.size(); |
| |
| mDeltaY = mDeltaYRange.clamp(deltaY); |
| mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1); |
| lp.y = mTempRange1.start; |
| lp.height = mTempRange1.size(); |
| |
| resizeWidgetIfNeeded(false); |
| |
| // When the widget resizes in multi-window mode, the translation value changes to maintain |
| // a center fit. These overrides ensure the resize frame always aligns with the widget view. |
| getSnappedRectRelativeToDragLayer(sTmpRect); |
| if (mLeftBorderActive) { |
| lp.width = sTmpRect.width() + sTmpRect.left - lp.x; |
| } |
| if (mTopBorderActive) { |
| lp.height = sTmpRect.height() + sTmpRect.top - lp.y; |
| } |
| if (mRightBorderActive) { |
| lp.x = sTmpRect.left; |
| } |
| if (mBottomBorderActive) { |
| lp.y = sTmpRect.top; |
| } |
| |
| // Handle invalid resize across CellLayouts in the two panel UI. |
| if (mCellLayout.getParent() instanceof Workspace) { |
| Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent(); |
| CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout); |
| if (pairedCellLayout != null) { |
| Rect focusedCellLayoutBound = sTmpRect; |
| mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound); |
| Rect resizeFrameBound = sTmpRect2; |
| findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound); |
| float progress = 1f; |
| if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout) |
| && mDeltaX < 0 |
| && resizeFrameBound.left < focusedCellLayoutBound.left) { |
| // Resize from right to left. |
| progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX) |
| / mDragAcrossTwoPanelOpacityMargin; |
| } else if (workspace.indexOfChild(pairedCellLayout) |
| > workspace.indexOfChild(mCellLayout) |
| && mDeltaX > 0 |
| && resizeFrameBound.right > focusedCellLayoutBound.right) { |
| // Resize from left to right. |
| progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX) |
| / mDragAcrossTwoPanelOpacityMargin; |
| } |
| float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress); |
| float springLoadedProgress = Math.min(1f, 1f - progress); |
| updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha, |
| springLoadedProgress); |
| } |
| } |
| |
| requestLayout(); |
| } |
| |
| private static int getSpanIncrement(float deltaFrac) { |
| return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0; |
| } |
| |
| /** |
| * Based on the current deltas, we determine if and how to resize the widget. |
| */ |
| private void resizeWidgetIfNeeded(boolean onDismiss) { |
| ViewGroup.LayoutParams wlp = mWidgetView.getLayoutParams(); |
| if (!(wlp instanceof CellLayoutLayoutParams)) { |
| return; |
| } |
| DeviceProfile dp = mLauncher.getDeviceProfile(); |
| float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x; |
| float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y; |
| |
| int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc); |
| int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc); |
| |
| if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; |
| |
| mDirectionVector[0] = 0; |
| mDirectionVector[1] = 0; |
| |
| CellLayoutLayoutParams lp = (CellLayoutLayoutParams) wlp; |
| |
| int spanX = lp.cellHSpan; |
| int spanY = lp.cellVSpan; |
| int cellX = lp.useTmpCoords ? lp.getTmpCellX() : lp.getCellX(); |
| int cellY = lp.useTmpCoords ? lp.getTmpCellY() : lp.getCellY(); |
| |
| // For each border, we bound the resizing based on the minimum width, and the maximum |
| // expandability. |
| mTempRange1.set(cellX, spanX + cellX); |
| int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive, |
| hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2); |
| cellX = mTempRange2.start; |
| spanX = mTempRange2.size(); |
| if (hSpanDelta != 0) { |
| mDirectionVector[0] = mLeftBorderActive ? -1 : 1; |
| } |
| |
| mTempRange1.set(cellY, spanY + cellY); |
| int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive, |
| vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2); |
| cellY = mTempRange2.start; |
| spanY = mTempRange2.size(); |
| if (vSpanDelta != 0) { |
| mDirectionVector[1] = mTopBorderActive ? -1 : 1; |
| } |
| |
| if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; |
| |
| // We always want the final commit to match the feedback, so we make sure to use the |
| // last used direction vector when committing the resize / reorder. |
| if (onDismiss) { |
| mDirectionVector[0] = mLastDirectionVector[0]; |
| mDirectionVector[1] = mLastDirectionVector[1]; |
| } else { |
| mLastDirectionVector[0] = mDirectionVector[0]; |
| mLastDirectionVector[1] = mDirectionVector[1]; |
| } |
| |
| if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, |
| mDirectionVector, onDismiss)) { |
| if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) { |
| mStateAnnouncer.announce( |
| mLauncher.getString(R.string.widget_resized, spanX, spanY)); |
| } |
| |
| lp.setTmpCellX(cellX); |
| lp.setTmpCellY(cellY); |
| lp.cellHSpan = spanX; |
| lp.cellVSpan = spanY; |
| mRunningVInc += vSpanDelta; |
| mRunningHInc += hSpanDelta; |
| |
| if (!onDismiss) { |
| WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); |
| } |
| } |
| mWidgetView.requestLayout(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| // We are done with resizing the widget. Save the widget size & position to LauncherModel |
| resizeWidgetIfNeeded(true); |
| mLauncher.getStatsLogManager() |
| .logger() |
| .withInstanceId(logInstanceId) |
| .withItemInfo((ItemInfo) mWidgetView.getTag()) |
| .log(LAUNCHER_WIDGET_RESIZE_COMPLETED); |
| } |
| |
| private void onTouchUp() { |
| DeviceProfile dp = mLauncher.getDeviceProfile(); |
| int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x; |
| int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y; |
| |
| mDeltaXAddOn = mRunningHInc * xThreshold; |
| mDeltaYAddOn = mRunningVInc * yThreshold; |
| mDeltaX = 0; |
| mDeltaY = 0; |
| |
| post(() -> snapToWidget(true)); |
| } |
| |
| /** |
| * Returns the rect of this view when the frame is snapped around the widget, with the bounds |
| * relative to the {@link DragLayer}. |
| */ |
| private void getSnappedRectRelativeToDragLayer(Rect out) { |
| float scale = mWidgetView.getScaleToFit(); |
| |
| mDragLayer.getViewRectRelativeToSelf(mWidgetView, out); |
| |
| int width = 2 * mBackgroundPadding |
| + (int) (scale * (out.width() - mWidgetPadding.left - mWidgetPadding.right)); |
| int height = 2 * mBackgroundPadding |
| + (int) (scale * (out.height() - mWidgetPadding.top - mWidgetPadding.bottom)); |
| |
| int x = (int) (out.left - mBackgroundPadding + scale * mWidgetPadding.left); |
| int y = (int) (out.top - mBackgroundPadding + scale * mWidgetPadding.top); |
| |
| out.left = x; |
| out.top = y; |
| out.right = out.left + width; |
| out.bottom = out.top + height; |
| } |
| |
| private void snapToWidget(boolean animate) { |
| getSnappedRectRelativeToDragLayer(sTmpRect); |
| int newWidth = sTmpRect.width(); |
| int newHeight = sTmpRect.height(); |
| int newX = sTmpRect.left; |
| int newY = sTmpRect.top; |
| |
| // We need to make sure the frame's touchable regions lie fully within the bounds of the |
| // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions |
| // down accordingly to provide a proper touch target. |
| if (newY < 0) { |
| // In this case we shift the touch region down to start at the top of the DragLayer |
| mTopTouchRegionAdjustment = -newY; |
| } else { |
| mTopTouchRegionAdjustment = 0; |
| } |
| if (newY + newHeight > mDragLayer.getHeight()) { |
| // In this case we shift the touch region up to end at the bottom of the DragLayer |
| mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); |
| } else { |
| mBottomTouchRegionAdjustment = 0; |
| } |
| |
| final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); |
| final CellLayout pairedCellLayout; |
| if (mCellLayout.getParent() instanceof Workspace) { |
| Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent(); |
| pairedCellLayout = workspace.getScreenPair(mCellLayout); |
| } else { |
| pairedCellLayout = null; |
| } |
| if (!animate) { |
| lp.width = newWidth; |
| lp.height = newHeight; |
| lp.x = newX; |
| lp.y = newY; |
| for (int i = 0; i < HANDLE_COUNT; i++) { |
| mDragHandles[i].setAlpha(1f); |
| } |
| if (pairedCellLayout != null) { |
| updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f, |
| /* springLoadedProgress= */ 0f); |
| } |
| requestLayout(); |
| } else { |
| ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp, |
| PropertyValuesHolder.ofInt(LAYOUT_WIDTH, lp.width, newWidth), |
| PropertyValuesHolder.ofInt(LAYOUT_HEIGHT, lp.height, newHeight), |
| PropertyValuesHolder.ofInt(LAYOUT_X, lp.x, newX), |
| PropertyValuesHolder.ofInt(LAYOUT_Y, lp.y, newY)); |
| mFirstFrameAnimatorHelper.addTo(oa).addUpdateListener(a -> requestLayout()); |
| |
| AnimatorSet set = new AnimatorSet(); |
| set.play(oa); |
| for (int i = 0; i < HANDLE_COUNT; i++) { |
| set.play(mFirstFrameAnimatorHelper.addTo( |
| ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f))); |
| } |
| if (pairedCellLayout != null) { |
| updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f, |
| /* springLoadedProgress= */ 0f, /* animatorSet= */ set); |
| } |
| set.setDuration(SNAP_DURATION); |
| set.start(); |
| } |
| |
| setFocusableInTouchMode(true); |
| requestFocus(); |
| } |
| |
| @Override |
| public boolean onKey(View v, int keyCode, KeyEvent event) { |
| // Clear the frame and give focus to the widget host view when a directional key is pressed. |
| if (shouldConsume(keyCode)) { |
| close(false); |
| mWidgetView.requestFocus(); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean handleTouchDown(MotionEvent ev) { |
| Rect hitRect = new Rect(); |
| int x = (int) ev.getX(); |
| int y = (int) ev.getY(); |
| |
| getHitRect(hitRect); |
| if (hitRect.contains(x, y)) { |
| if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) { |
| mXDown = x; |
| mYDown = y; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isTouchOnReconfigureButton(MotionEvent ev) { |
| int xFrame = (int) ev.getX() - getLeft(); |
| int yFrame = (int) ev.getY() - getTop(); |
| mReconfigureButton.getHitRect(sTmpRect); |
| return sTmpRect.contains(xFrame, yFrame); |
| } |
| |
| @Override |
| public boolean onControllerTouchEvent(MotionEvent ev) { |
| int action = ev.getAction(); |
| int x = (int) ev.getX(); |
| int y = (int) ev.getY(); |
| |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| return handleTouchDown(ev); |
| case MotionEvent.ACTION_MOVE: |
| visualizeResizeForDelta(x - mXDown, y - mYDown); |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| visualizeResizeForDelta(x - mXDown, y - mYDown); |
| onTouchUp(); |
| mXDown = mYDown = 0; |
| break; |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onControllerInterceptTouchEvent(MotionEvent ev) { |
| if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) { |
| return true; |
| } |
| // Keep the resize frame open but let a click on the reconfigure button fall through to the |
| // button's OnClickListener. |
| if (isTouchOnReconfigureButton(ev)) { |
| return false; |
| } |
| close(false); |
| return false; |
| } |
| |
| @Override |
| protected void handleClose(boolean animate) { |
| mDragLayer.removeView(this); |
| } |
| |
| private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout, |
| float alpha, float springLoadedProgress) { |
| updateInvalidResizeEffect(cellLayout, pairedCellLayout, alpha, |
| springLoadedProgress, /* animatorSet= */ null); |
| } |
| |
| private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout, |
| float alpha, float springLoadedProgress, @Nullable AnimatorSet animatorSet) { |
| int childCount = pairedCellLayout.getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| View child = pairedCellLayout.getChildAt(i); |
| if (animatorSet != null) { |
| animatorSet.play( |
| mFirstFrameAnimatorHelper.addTo( |
| ObjectAnimator.ofFloat(child, ALPHA, alpha))); |
| } else { |
| child.setAlpha(alpha); |
| } |
| } |
| if (animatorSet != null) { |
| animatorSet.play(mFirstFrameAnimatorHelper.addTo( |
| ObjectAnimator.ofFloat(cellLayout, SPRING_LOADED_PROGRESS, |
| springLoadedProgress))); |
| animatorSet.play(mFirstFrameAnimatorHelper.addTo( |
| ObjectAnimator.ofFloat(pairedCellLayout, SPRING_LOADED_PROGRESS, |
| springLoadedProgress))); |
| } else { |
| cellLayout.setSpringLoadedProgress(springLoadedProgress); |
| pairedCellLayout.setSpringLoadedProgress(springLoadedProgress); |
| } |
| |
| boolean shouldShowCellLayoutBorder = springLoadedProgress > 0f; |
| if (animatorSet != null) { |
| animatorSet.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); |
| pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); |
| } |
| }); |
| } else { |
| cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); |
| pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder); |
| } |
| } |
| |
| @Override |
| protected boolean isOfType(int type) { |
| return (type & TYPE_WIDGET_RESIZE_FRAME) != 0; |
| } |
| |
| /** |
| * A mutable class for describing the range of two int values. |
| */ |
| private static class IntRange { |
| |
| public int start, end; |
| |
| public int clamp(int value) { |
| return Utilities.boundToRange(value, start, end); |
| } |
| |
| public void set(int s, int e) { |
| start = s; |
| end = e; |
| } |
| |
| public int size() { |
| return end - start; |
| } |
| |
| /** |
| * Moves either the start or end edge (but never both) by {@param delta} and sets the |
| * result in {@param out} |
| */ |
| public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) { |
| out.start = moveStart ? start + delta : start; |
| out.end = moveEnd ? end + delta : end; |
| } |
| |
| /** |
| * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)}, |
| * with extra conditions. |
| * @param minSize minimum size after with the moving edge should not be shifted any further. |
| * For eg, if delta = -3 when moving the endEdge brings the size to less than |
| * minSize, only delta = -2 will applied |
| * @param maxSize maximum size after with the moving edge should not be shifted any further. |
| * For eg, if delta = -3 when moving the endEdge brings the size to greater |
| * than maxSize, only delta = -2 will applied |
| * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0) |
| * @return the amount of increase when endEdge was moves and the amount of decrease when |
| * the start edge was moved. |
| */ |
| public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, |
| int minSize, int maxSize, int maxEnd, IntRange out) { |
| applyDelta(moveStart, moveEnd, delta, out); |
| if (out.start < 0) { |
| out.start = 0; |
| } |
| if (out.end > maxEnd) { |
| out.end = maxEnd; |
| } |
| if (out.size() < minSize) { |
| if (moveStart) { |
| out.start = out.end - minSize; |
| } else if (moveEnd) { |
| out.end = out.start + minSize; |
| } |
| } |
| if (out.size() > maxSize) { |
| if (moveStart) { |
| out.start = out.end - maxSize; |
| } else if (moveEnd) { |
| out.end = out.start + maxSize; |
| } |
| } |
| return moveEnd ? out.size() - size() : size() - out.size(); |
| } |
| } |
| |
| /** |
| * Returns true only if this utility class handles the key code. |
| */ |
| public static boolean shouldConsume(int keyCode) { |
| return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT |
| || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN |
| || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END |
| || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN); |
| } |
| |
| @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() { |
| Rect rect = new Rect(); |
| if (!mReconfigureButton.getGlobalVisibleRect(rect)) { |
| return null; |
| } |
| @Px int tipMargin = mLauncher.getResources() |
| .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin); |
| return new ArrowTipView(mLauncher, /* isPointingUp= */ true) |
| .showAroundRect( |
| getContext().getString(R.string.reconfigurable_widget_education_tip), |
| /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2, |
| /* rect= */ rect, |
| /* margin= */ tipMargin); |
| } |
| |
| private boolean hasSeenReconfigurableWidgetEducationTip() { |
| return mLauncher.getSharedPrefs() |
| .getBoolean(KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, false) |
| || Utilities.isRunningInTestHarness(); |
| } |
| } |