Files
Lawnchair/src/com/android/launcher3/folder/FolderPagedView.java
T
Vinit Nayak a406f727ad Add vertical layout support for Overview in portrait
WM is making changes which allows apps to maintain
their orientation independent of the orientation of
the foreground app. This allows recents to always start
in portrait even when the app currently running is in
landscape. This means we have to give the illusion of
a landscape oriented overview when user swipes up in
gesterual nav when launcher is started in portrait
configuration.

PagedOrientationHandler abstracts all coordinate specific
logic from Paged/RecentsView primarily, but also all
other dynamic calculations throughout launcher.
PagedViewOrientationState is the single point of exposure
to other classes that depend on those changes. The goal
is to also minimize holding state to allow for default
implementations of PagedOrientationHandler for all the
3p/Fallback classes. PagedViewOrientationState also
holds other data around rotation that isn't
specifically tied to view logic.

The fake landscape overview can be toggled with:
adb shell settings put global forced_rotation [0/1]

Fixes: 146176182
Change-Id: I65d8d4e9f92b93931cbe0053ccaf0cda8d2ffd6c
2020-03-02 18:02:35 -08:00

621 lines
21 KiB
Java

/*
* 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.folder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
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.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.pageindicators.PageIndicatorDots;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.ViewCache;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
public class FolderPagedView extends PagedView<PageIndicatorDots> {
private static final String TAG = "FolderPagedView";
private static final int REORDER_ANIMATION_DURATION = 230;
private static final int START_VIEW_REORDER_DELAY = 30;
private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
/**
* Fraction of the width to scroll when showing the next page hint.
*/
private static final float SCROLL_HINT_FRACTION = 0.07f;
private static final int[] sTmpArray = new int[2];
public final boolean mIsRtl;
private final ViewGroupFocusHelper mFocusIndicatorHelper;
@Thunk final ArrayMap<View, Runnable> mPendingAnimations = new ArrayMap<>();
private final FolderGridOrganizer mOrganizer;
private final ViewCache mViewCache;
private int mAllocatedContentSize;
@ViewDebug.ExportedProperty(category = "launcher")
private int mGridCountX;
@ViewDebug.ExportedProperty(category = "launcher")
private int mGridCountY;
private Folder mFolder;
// If the views are attached to the folder or not. A folder should be bound when its
// animating or is open.
private boolean mViewsBound = false;
public FolderPagedView(Context context, AttributeSet attrs) {
super(context, attrs);
InvariantDeviceProfile profile = LauncherAppState.getIDP(context);
mOrganizer = new FolderGridOrganizer(profile);
mIsRtl = Utilities.isRtl(getResources());
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
mViewCache = BaseActivity.fromContext(context).getViewCache();
}
public void setFolder(Folder folder) {
mFolder = folder;
mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
initParentViews(folder);
}
/**
* Sets up the grid size such that {@param count} items can fit in the grid.
*/
private void setupContentDimensions(int count) {
mAllocatedContentSize = count;
mOrganizer.setContentSize(count);
mGridCountX = mOrganizer.getCountX();
mGridCountY = mOrganizer.getCountY();
// Update grid size
for (int i = getPageCount() - 1; i >= 0; i--) {
getPageAt(i).setGridSize(mGridCountX, mGridCountY);
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
mFocusIndicatorHelper.draw(canvas);
super.dispatchDraw(canvas);
}
/**
* Binds items to the layout.
*/
public void bindItems(List<WorkspaceItemInfo> items) {
if (mViewsBound) {
unbindItems();
}
arrangeChildren(items.stream().map(this::createNewView).collect(Collectors.toList()));
mViewsBound = true;
}
/**
* Removes all the icons from the folder
*/
public void unbindItems() {
for (int i = getChildCount() - 1; i >= 0; i--) {
CellLayout page = (CellLayout) getChildAt(i);
ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets();
for (int j = container.getChildCount() - 1; j >= 0; j--) {
mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j));
}
page.removeAllViews();
mViewCache.recycleView(R.layout.folder_page, page);
}
removeAllViews();
mViewsBound = false;
}
/**
* Returns true if the icons are bound to the folder
*/
public boolean areViewsBound() {
return mViewsBound;
}
/**
* Creates and adds an icon corresponding to the provided rank
* @return the created icon
*/
public View createAndAddViewForRank(WorkspaceItemInfo item, int rank) {
View icon = createNewView(item);
if (!mViewsBound) {
return icon;
}
ArrayList<View> views = new ArrayList<>(mFolder.getIconsInReadingOrder());
views.add(rank, icon);
arrangeChildren(views);
return icon;
}
/**
* Adds the {@param view} to the layout based on {@param rank} and updated the position
* related attributes. It assumes that {@param item} is already attached to the view.
*/
public void addViewForRank(View view, WorkspaceItemInfo item, int rank) {
int pageNo = rank / mOrganizer.getMaxItemsPerPage();
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
lp.setXY(mOrganizer.getPosForRank(rank));
getPageAt(pageNo).addViewToCellLayout(view, -1, item.getViewId(), lp, true);
}
@SuppressLint("InflateParams")
public View createNewView(WorkspaceItemInfo item) {
if (item == null) {
return null;
}
final BubbleTextView textView = mViewCache.getView(
R.layout.folder_application, getContext(), null);
textView.applyFromWorkspaceItem(item);
textView.setOnClickListener(ItemClickHandler.INSTANCE);
textView.setOnLongClickListener(mFolder);
textView.setOnFocusChangeListener(mFocusIndicatorHelper);
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) textView.getLayoutParams();
if (lp == null) {
textView.setLayoutParams(new CellLayout.LayoutParams(
item.cellX, item.cellY, item.spanX, item.spanY));
} else {
lp.cellX = item.cellX;
lp.cellY = item.cellY;
lp.cellHSpan = lp.cellVSpan = 1;
}
return textView;
}
@Override
public CellLayout getPageAt(int index) {
return (CellLayout) getChildAt(index);
}
public CellLayout getCurrentCellLayout() {
return getPageAt(getNextPage());
}
private CellLayout createAndAddNewPage() {
DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this);
page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
page.setInvertIfRtl(true);
page.setGridSize(mGridCountX, mGridCountY);
addView(page, -1, generateDefaultLayoutParams());
return page;
}
@Override
protected int getChildGap() {
return getPaddingLeft() + getPaddingRight();
}
public void setFixedSize(int width, int height) {
width -= (getPaddingLeft() + getPaddingRight());
height -= (getPaddingTop() + getPaddingBottom());
for (int i = getChildCount() - 1; i >= 0; i --) {
((CellLayout) getChildAt(i)).setFixedSize(width, height);
}
}
public void removeItem(View v) {
for (int i = getChildCount() - 1; i >= 0; i --) {
getPageAt(i).removeView(v);
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
mPageIndicator.setScroll(l, mMaxScroll);
}
/**
* Updates position and rank of all the children in the view.
* It essentially removes all views from all the pages and then adds them again in appropriate
* page.
*
* @param list the ordered list of children.
*/
@SuppressLint("RtlHardcoded")
public void arrangeChildren(List<View> list) {
int itemCount = list.size();
ArrayList<CellLayout> pages = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
CellLayout page = (CellLayout) getChildAt(i);
page.removeAllViews();
pages.add(page);
}
mOrganizer.setFolderInfo(mFolder.getInfo());
setupContentDimensions(itemCount);
Iterator<CellLayout> pageItr = pages.iterator();
CellLayout currentPage = null;
int position = 0;
int rank = 0;
for (int i = 0; i < itemCount; i++) {
View v = list.size() > i ? list.get(i) : null;
if (currentPage == null || position >= mOrganizer.getMaxItemsPerPage()) {
// Next page
if (pageItr.hasNext()) {
currentPage = pageItr.next();
} else {
currentPage = createAndAddNewPage();
}
position = 0;
}
if (v != null) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
ItemInfo info = (ItemInfo) v.getTag();
lp.setXY(mOrganizer.getPosForRank(rank));
currentPage.addViewToCellLayout(v, -1, info.getViewId(), lp, true);
if (mOrganizer.isItemInPreview(rank) && v instanceof BubbleTextView) {
((BubbleTextView) v).verifyHighRes();
}
}
rank++;
position++;
}
// Remove extra views.
boolean removed = false;
while (pageItr.hasNext()) {
removeView(pageItr.next());
removed = true;
}
if (removed) {
setCurrentPage(0);
}
setEnableOverscroll(getPageCount() > 1);
// Update footer
mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
// Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
mFolder.mFolderName.setGravity(getPageCount() > 1 ?
(mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
}
public int getDesiredWidth() {
return getPageCount() > 0 ?
(getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0;
}
public int getDesiredHeight() {
return getPageCount() > 0 ?
(getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0;
}
/**
* @return the rank of the cell nearest to the provided pixel position.
*/
public int findNearestArea(int pixelX, int pixelY) {
int pageIndex = getNextPage();
CellLayout page = getPageAt(pageIndex);
page.findNearestArea(pixelX, pixelY, 1, 1, sTmpArray);
if (mFolder.isLayoutRtl()) {
sTmpArray[0] = page.getCountX() - sTmpArray[0] - 1;
}
return Math.min(mAllocatedContentSize - 1,
pageIndex * mOrganizer.getMaxItemsPerPage()
+ sTmpArray[1] * mGridCountX + sTmpArray[0]);
}
public View getFirstItem() {
return getViewInCurrentPage(c -> 0);
}
public View getLastItem() {
return getViewInCurrentPage(c -> c.getChildCount() - 1);
}
private View getViewInCurrentPage(ToIntFunction<ShortcutAndWidgetContainer> rankProvider) {
if (getChildCount() < 1) {
return null;
}
ShortcutAndWidgetContainer container = getCurrentCellLayout().getShortcutsAndWidgets();
int rank = rankProvider.applyAsInt(container);
if (mGridCountX > 0) {
return container.getChildAt(rank % mGridCountX, rank / mGridCountX);
} else {
return container.getChildAt(rank);
}
}
/**
* Iterates over all its items in a reading order.
* @return the view for which the operator returned true.
*/
public View iterateOverItems(ItemOperator op) {
for (int k = 0 ; k < getChildCount(); k++) {
CellLayout page = getPageAt(k);
for (int j = 0; j < page.getCountY(); j++) {
for (int i = 0; i < page.getCountX(); i++) {
View v = page.getChildAt(i, j);
if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v)) {
return v;
}
}
}
}
return null;
}
public String getAccessibilityDescription() {
return getContext().getString(R.string.folder_opened, mGridCountX, mGridCountY);
}
/**
* Sets the focus on the first visible child.
*/
public void setFocusOnFirstChild() {
View firstChild = getCurrentCellLayout().getChildAt(0, 0);
if (firstChild != null) {
firstChild.requestFocus();
}
}
@Override
protected void notifyPageSwitchListener(int prevPage) {
super.notifyPageSwitchListener(prevPage);
if (mFolder != null) {
mFolder.updateTextViewFocus();
}
}
/**
* Scrolls the current view by a fraction
*/
public void showScrollHint(int direction) {
float fraction = (direction == Folder.SCROLL_LEFT) ^ mIsRtl
? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION;
int hint = (int) (fraction * getWidth());
int scroll = getScrollForPage(getNextPage()) + hint;
int delta = scroll - getScrollX();
if (delta != 0) {
mScroller.setInterpolator(Interpolators.DEACCEL);
mScroller.startScroll(getScrollX(), delta, Folder.SCROLL_HINT_DURATION);
invalidate();
}
}
public void clearScrollHint() {
if (getScrollX() != getScrollForPage(getNextPage())) {
snapToPage(getNextPage());
}
}
/**
* Finish animation all the views which are animating across pages
*/
public void completePendingPageChanges() {
if (!mPendingAnimations.isEmpty()) {
ArrayMap<View, Runnable> pendingViews = new ArrayMap<>(mPendingAnimations);
for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) {
e.getKey().animate().cancel();
e.getValue().run();
}
}
}
public boolean rankOnCurrentPage(int rank) {
int p = rank / mOrganizer.getMaxItemsPerPage();
return p == getNextPage();
}
@Override
protected void onPageBeginTransition() {
super.onPageBeginTransition();
// Ensure that adjacent pages have high resolution icons
verifyVisibleHighResIcons(getCurrentPage() - 1);
verifyVisibleHighResIcons(getCurrentPage() + 1);
}
/**
* Ensures that all the icons on the given page are of high-res
*/
public void verifyVisibleHighResIcons(int pageNo) {
CellLayout page = getPageAt(pageNo);
if (page != null) {
ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
BubbleTextView icon = ((BubbleTextView) parent.getChildAt(i));
icon.verifyHighRes();
// Set the callback back to the actual icon, in case
// it was captured by the FolderIcon
Drawable d = icon.getCompoundDrawables()[1];
if (d != null) {
d.setCallback(icon);
}
}
}
}
public int getAllocatedContentSize() {
return mAllocatedContentSize;
}
/**
* Reorders the items such that the {@param empty} spot moves to {@param target}
*/
public void realTimeReorder(int empty, int target) {
completePendingPageChanges();
int delay = 0;
float delayAmount = START_VIEW_REORDER_DELAY;
// Animation only happens on the current page.
int pageToAnimate = getNextPage();
int maxItemsPerPage = mOrganizer.getMaxItemsPerPage();
int pageT = target / maxItemsPerPage;
int pagePosT = target % maxItemsPerPage;
if (pageT != pageToAnimate) {
Log.e(TAG, "Cannot animate when the target cell is invisible");
}
int pagePosE = empty % maxItemsPerPage;
int pageE = empty / maxItemsPerPage;
int startPos, endPos;
int moveStart, moveEnd;
int direction;
if (target == empty) {
// No animation
return;
} else if (target > empty) {
// Items will move backwards to make room for the empty cell.
direction = 1;
// If empty cell is in a different page, move them instantly.
if (pageE < pageToAnimate) {
moveStart = empty;
// Instantly move the first item in the current page.
moveEnd = pageToAnimate * maxItemsPerPage;
// Animate the 2nd item in the current page, as the first item was already moved to
// the last page.
startPos = 0;
} else {
moveStart = moveEnd = -1;
startPos = pagePosE;
}
endPos = pagePosT;
} else {
// The items will move forward.
direction = -1;
if (pageE > pageToAnimate) {
// Move the items immediately.
moveStart = empty;
// Instantly move the last item in the current page.
moveEnd = (pageToAnimate + 1) * maxItemsPerPage - 1;
// Animations start with the second last item in the page
startPos = maxItemsPerPage - 1;
} else {
moveStart = moveEnd = -1;
startPos = pagePosE;
}
endPos = pagePosT;
}
// Instant moving views.
while (moveStart != moveEnd) {
int rankToMove = moveStart + direction;
int p = rankToMove / maxItemsPerPage;
int pagePos = rankToMove % maxItemsPerPage;
int x = pagePos % mGridCountX;
int y = pagePos / mGridCountX;
final CellLayout page = getPageAt(p);
final View v = page.getChildAt(x, y);
if (v != null) {
if (pageToAnimate != p) {
page.removeView(v);
addViewForRank(v, (WorkspaceItemInfo) v.getTag(), moveStart);
} else {
// Do a fake animation before removing it.
final int newRank = moveStart;
final float oldTranslateX = v.getTranslationX();
Runnable endAction = new Runnable() {
@Override
public void run() {
mPendingAnimations.remove(v);
v.setTranslationX(oldTranslateX);
((CellLayout) v.getParent().getParent()).removeView(v);
addViewForRank(v, (WorkspaceItemInfo) v.getTag(), newRank);
}
};
v.animate()
.translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth())
.setDuration(REORDER_ANIMATION_DURATION)
.setStartDelay(0)
.withEndAction(endAction);
mPendingAnimations.put(v, endAction);
}
}
moveStart = rankToMove;
}
if ((endPos - startPos) * direction <= 0) {
// No animation
return;
}
CellLayout page = getPageAt(pageToAnimate);
for (int i = startPos; i != endPos; i += direction) {
int nextPos = i + direction;
View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
REORDER_ANIMATION_DURATION, delay, true, true)) {
delay += delayAmount;
delayAmount *= VIEW_REORDER_DELAY_FACTOR;
}
}
}
public int itemsPerPage() {
return mOrganizer.getMaxItemsPerPage();
}
}