Decouple the scaling / translation of widget views necessary in split-screen

=> We pull apart the scale and translation that are set in a fairly
   static way due to split-screen vs. the general translation and scale
   properties that might be used more dynamically, in this case
   for re-order animations
=> This allows removal of some code that breaks reorder animations
   due to the accrual of translations / scales in certain edge cases.
=> TODO in future CL: address other translation cases and make the throw
   case for calling base setTranslationX/Y for Workspace Items unconditional

issue 149438360

test: manual
Change-Id: Ic3fde172f669e215cd25db0fcd4e1c3c873d314f
This commit is contained in:
Adam Cohen 2020-03-24 16:35:35 -07:00
parent c09e8fbe9d
commit d916206271
6 changed files with 223 additions and 114 deletions

View File

@ -29,6 +29,7 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@ -70,7 +71,7 @@ import java.text.NumberFormat;
* too aggressive.
*/
public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
IconLabelDotView, DraggableView {
IconLabelDotView, DraggableView, Reorderable {
private static final int DISPLAY_WORKSPACE = 0;
private static final int DISPLAY_ALL_APPS = 1;
@ -78,6 +79,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
private final PointF mTranslationForReorder = new PointF(0, 0);
private float mScaleForReorder = 1f;
private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
= new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
@ -672,6 +675,30 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
return mIconSize;
}
public void setReorderOffset(float x, float y) {
mTranslationForReorder.set(x, y);
super.setTranslationX(x);
super.setTranslationY(y);
}
public void getReorderOffset(PointF offset) {
offset.set(mTranslationForReorder);
}
public void setReorderScale(float scale) {
mScaleForReorder = scale;
super.setScaleX(scale);
super.setScaleY(scale);
}
public float getReorderScale() {
return mScaleForReorder;
}
public View getView() {
return this;
}
@Override
public int getViewType() {
return DRAGGABLE_ICON;

View File

@ -33,6 +33,7 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@ -54,7 +55,6 @@ import androidx.core.view.ViewCompat;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PropertyListBuilder;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.PreviewBackground;
@ -66,7 +66,6 @@ import com.android.launcher3.util.ParcelableSparseArray;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -99,6 +98,7 @@ public class CellLayout extends ViewGroup {
// return an (x, y) value from helper functions. Do NOT use them to maintain other state.
@Thunk final int[] mTmpPoint = new int[2];
@Thunk final int[] mTempLocation = new int[2];
final PointF mTmpPointF = new PointF();
// Used to visualize / debug the Grid of the CellLayout
private static final boolean VISUALIZE_GRID = false;
@ -136,7 +136,7 @@ public class CellLayout extends ViewGroup {
private final Paint mDragOutlinePaint = new Paint();
@Thunk final ArrayMap<LayoutParams, Animator> mReorderAnimators = new ArrayMap<>();
@Thunk final ArrayMap<View, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
@Thunk final ArrayMap<Reorderable, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
private boolean mItemPlacementDirty = false;
@ -1868,10 +1868,11 @@ public class CellLayout extends ViewGroup {
boolean skip = mode == ReorderPreviewAnimation.MODE_HINT && solution.intersectingViews
!= null && !solution.intersectingViews.contains(child);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (c != null && !skip) {
ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode, lp.cellX,
lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
if (c != null && !skip && (child instanceof Reorderable)) {
ReorderPreviewAnimation rha = new ReorderPreviewAnimation((Reorderable) child,
mode, lp.cellX, lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
rha.animate();
}
}
@ -1893,7 +1894,7 @@ public class CellLayout extends ViewGroup {
// Class which represents the reorder preview animations. These animations show that an item is
// in a temporary state, and hint at where the item will return to.
class ReorderPreviewAnimation {
final View child;
final Reorderable child;
float finalDeltaX;
float finalDeltaY;
float initDeltaX;
@ -1913,8 +1914,8 @@ public class CellLayout extends ViewGroup {
float animationProgress = 0;
ValueAnimator a;
public ReorderPreviewAnimation(View child, int mode, int cellX0, int cellY0, int cellX1,
int cellY1, int spanX, int spanY) {
public ReorderPreviewAnimation(Reorderable child, int mode, int cellX0, int cellY0,
int cellX1, int cellY1, int spanX, int spanY) {
regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
final int x0 = mTmpPoint[0];
final int y0 = mTmpPoint[1];
@ -1926,63 +1927,60 @@ public class CellLayout extends ViewGroup {
this.child = child;
this.mode = mode;
finalDeltaX = 0;
finalDeltaY = 0;
child.getReorderOffset(mTmpPointF);
initDeltaX = mTmpPointF.x;
initDeltaY = mTmpPointF.y;
initScale = child.getReorderScale();
finalScale = mChildScale - (CHILD_DIVIDEND / child.getView().getWidth()) * initScale;
// TODO issue!
setInitialAnimationValues(false);
finalScale = (mChildScale - (CHILD_DIVIDEND / child.getWidth())) * initScale;
finalDeltaX = initDeltaX;
finalDeltaY = initDeltaY;
int dir = mode == MODE_HINT ? -1 : 1;
if (dX == dY && dX == 0) {
} else {
if (dY == 0) {
finalDeltaX += - dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
} else if (dX == 0) {
finalDeltaY += - dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
} else {
double angle = Math.atan( (float) (dY) / dX);
finalDeltaX += (int) (- dir * Math.signum(dX) *
Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
finalDeltaY += (int) (- dir * Math.signum(dY) *
Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
finalDeltaX = (int) (-dir * Math.signum(dX)
* Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
finalDeltaY = (int) (-dir * Math.signum(dY)
* Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
}
}
}
void setInitialAnimationValues(boolean restoreOriginalValues) {
if (restoreOriginalValues) {
if (child instanceof LauncherAppWidgetHostView) {
LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
initScale = lahv.getScaleToFit();
initDeltaX = lahv.getTranslationForCentering().x;
initDeltaY = lahv.getTranslationForCentering().y;
} else {
initScale = mChildScale;
initDeltaX = 0;
initDeltaY = 0;
}
} else {
initScale = child.getScaleX();
initDeltaX = child.getTranslationX();
initDeltaY = child.getTranslationY();
}
void setInitialAnimationValuesToBaseline() {
initScale = mChildScale;
initDeltaX = 0;
initDeltaY = 0;
}
void animate() {
boolean noMovement = (finalDeltaX == initDeltaX) && (finalDeltaY == initDeltaY);
boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0);
if (mShakeAnimators.containsKey(child)) {
ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child);
oldAnimation.cancel();
mShakeAnimators.remove(child);
if (noMovement) {
completeAnimationImmediately();
// A previous animation for this item exists, and no new animation will exist.
// Finish the old animation smoothly.
oldAnimation.finishAnimation();
return;
} else {
// A previous animation for this item exists, and a new one will exist. Stop
// the old animation in its tracks, and proceed with the new one.
oldAnimation.cancel();
}
}
if (noMovement) {
return;
}
ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS, 0, 1);
a = va;
@ -1999,7 +1997,7 @@ public class CellLayout extends ViewGroup {
va.addListener(new AnimatorListenerAdapter() {
public void onAnimationRepeat(Animator animation) {
// We make sure to end only after a full period
setInitialAnimationValues(true);
setInitialAnimationValuesToBaseline();
repeating = true;
}
});
@ -2012,11 +2010,9 @@ public class CellLayout extends ViewGroup {
float r1 = (mode == MODE_HINT && repeating) ? 1.0f : animationProgress;
float x = r1 * finalDeltaX + (1 - r1) * initDeltaX;
float y = r1 * finalDeltaY + (1 - r1) * initDeltaY;
child.setTranslationX(x);
child.setTranslationY(y);
child.setReorderOffset(x, y);
float s = animationProgress * finalScale + (1 - animationProgress) * initScale;
child.setScaleX(s);
child.setScaleY(s);
child.setReorderScale(s);
}
private void cancel() {
@ -2025,27 +2021,27 @@ public class CellLayout extends ViewGroup {
}
}
@Thunk void completeAnimationImmediately() {
/**
* Smoothly returns the item to its baseline position / scale
*/
@Thunk void finishAnimation() {
if (a != null) {
a.cancel();
}
setInitialAnimationValues(true);
a = new PropertyListBuilder()
.scale(initScale)
.translationX(initDeltaX)
.translationY(initDeltaY)
.build(child)
.setDuration(REORDER_ANIMATION_DURATION);
Launcher.cast(mActivity).getDragController().addFirstFrameAnimationHelper(a);
setInitialAnimationValuesToBaseline();
ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS,
animationProgress, 0);
a = va;
a.setInterpolator(DEACCEL_1_5);
a.setDuration(REORDER_ANIMATION_DURATION);
a.start();
}
}
private void completeAndClearReorderPreviewAnimations() {
for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
a.completeAnimationImmediately();
a.finishAnimation();
}
mShakeAnimators.clear();
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2020 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;
import android.graphics.PointF;
import android.view.View;
public interface Reorderable {
/**
* Set the offset related to reorder hint and "bounce" animations
*/
void setReorderOffset(float x, float y);
void getReorderOffset(PointF offset);
/**
* Set the scale related to reorder hint and "bounce" animations
*/
void setReorderScale(float scale);
float getReorderScale();
/**
* Get the com.android.view related to this object
*/
View getView();
}

View File

@ -26,6 +26,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
@ -49,6 +50,7 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.OnAlarmListener;
import com.android.launcher3.R;
import com.android.launcher3.Reorderable;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.anim.Interpolators;
@ -79,7 +81,7 @@ import java.util.function.Predicate;
* An icon that can appear on in the workspace representing an {@link Folder}.
*/
public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
DraggableView {
DraggableView, Reorderable {
@Thunk ActivityContext mActivity;
@Thunk Folder mFolder;
@ -119,6 +121,9 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
private float mDotScale;
private Animator mDotScaleAnim;
private final PointF mTranslationForReorder = new PointF(0, 0);
private float mScaleForReorder = 1f;
private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY
= new Property<FolderIcon, Float>(Float.TYPE, "dotScale") {
@Override
@ -226,16 +231,6 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
mBackground.getBounds(outBounds);
}
@Override
public int getViewType() {
return DRAGGABLE_ICON;
}
@Override
public void getVisualDragBounds(Rect bounds) {
getPreviewBounds(bounds);
}
public float getBackgroundStrokeWidth() {
return mBackground.getStrokeWidth();
}
@ -716,4 +711,39 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
public void onFolderClose(int currentPage) {
mPreviewItemManager.onFolderClose(currentPage);
}
public void setReorderOffset(float x, float y) {
mTranslationForReorder.set(x, y);
super.setTranslationX(x);
super.setTranslationY(y);
}
public void getReorderOffset(PointF offset) {
offset.set(mTranslationForReorder);
}
public void setReorderScale(float scale) {
mScaleForReorder = scale;
super.setScaleX(scale);
super.setScaleY(scale);
}
public float getReorderScale() {
return mScaleForReorder;
}
public View getView() {
return this;
}
@Override
public int getViewType() {
return DRAGGABLE_ICON;
}
@Override
public void getVisualDragBounds(Rect bounds) {
getPreviewBounds(bounds);
}
}

View File

@ -19,8 +19,6 @@ package com.android.launcher3.widget;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.os.SystemClock;
import android.util.SparseBooleanArray;
@ -40,7 +38,6 @@ import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.Executors;
@ -51,7 +48,7 @@ import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
* {@inheritDoc}
*/
public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
implements TouchCompleteListener, View.OnLongClickListener, DraggableView {
implements TouchCompleteListener, View.OnLongClickListener {
// Related to the auto-advancing of widgets
private static final long ADVANCE_INTERVAL = 20000;
@ -73,15 +70,7 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
private boolean mIsAutoAdvanceRegistered;
private Runnable mAutoAdvanceRunnable;
/**
* The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
*/
private float mScaleToFit = 1f;
/**
* The translation values to center the widget within its cellspans.
*/
private final PointF mTranslationForCentering = new PointF(0, 0);
public LauncherAppWidgetHostView(Context context) {
super(context);
@ -307,26 +296,6 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
scheduleNextAdvance();
}
public void setScaleToFit(float scale) {
mScaleToFit = scale;
setScaleX(scale);
setScaleY(scale);
}
public float getScaleToFit() {
return mScaleToFit;
}
public void setTranslationForCentering(float x, float y) {
mTranslationForCentering.set(x, y);
setTranslationX(x);
setTranslationY(y);
}
public PointF getTranslationForCentering() {
return mTranslationForCentering;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@ -357,17 +326,4 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
}
return false;
}
@Override
public int getViewType() {
return DRAGGABLE_WIDGET;
}
@Override
public void getVisualDragBounds(Rect bounds) {
int width = (int) (getMeasuredWidth() * mScaleToFit);
int height = (int) (getMeasuredHeight() * mScaleToFit);
bounds.set(0, 0 , width, height);
}
}

View File

@ -18,12 +18,14 @@ package com.android.launcher3.widget;
import android.appwidget.AppWidgetHostView;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import com.android.launcher3.Reorderable;
import com.android.launcher3.dragndrop.DraggableView;
import java.util.ArrayList;
@ -32,7 +34,20 @@ import java.util.ArrayList;
* Extension of AppWidgetHostView with support for controlled keyboard navigation.
*/
public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
implements DraggableView {
implements DraggableView, Reorderable {
/**
* The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
*/
private float mScaleToFit = 1f;
/**
* The translation values to center the widget within its cellspans.
*/
private final PointF mTranslationForCentering = new PointF(0, 0);
private final PointF mTranslationForReorder = new PointF(0, 0);
private float mScaleForReorder = 1f;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mChildrenFocused;
@ -137,6 +152,47 @@ public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
setSelected(childIsFocused);
}
public void setScaleToFit(float scale) {
mScaleToFit = scale;
super.setScaleX(scale * mScaleForReorder);
super.setScaleY(scale * mScaleForReorder);
}
public float getScaleToFit() {
return mScaleToFit;
}
public View getView() {
return this;
}
public void setTranslationForCentering(float x, float y) {
mTranslationForCentering.set(x, y);
super.setTranslationX(x + mTranslationForReorder.x);
super.setTranslationY(y + mTranslationForReorder.y);
}
public void setReorderOffset(float x, float y) {
mTranslationForReorder.set(x, y);
super.setTranslationX(mTranslationForCentering.x + x);
super.setTranslationY(mTranslationForCentering.y + y);
}
public void getReorderOffset(PointF offset) {
offset.set(mTranslationForReorder);
}
public void setReorderScale(float scale) {
mScaleForReorder = scale;
super.setScaleX(mScaleToFit * scale);
super.setScaleY(mScaleToFit * scale);
}
public float getReorderScale() {
return mScaleForReorder;
}
@Override
public int getViewType() {
return DRAGGABLE_WIDGET;
@ -144,6 +200,9 @@ public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
@Override
public void getVisualDragBounds(Rect bounds) {
bounds.set(0, 0 , getMeasuredWidth(), getMeasuredHeight());
int width = (int) (getMeasuredWidth() * mScaleToFit);
int height = (int) (getMeasuredHeight() * mScaleToFit);
bounds.set(0, 0 , width, height);
}
}