Files
Lawnchair/src/com/android/launcher3/allapps/FloatingHeaderView.java
T
Andy Wickham 14101659f7 Reduces search clipping when work profile is not present.
Basically, adds the spacing that is otherwise included by
the tab protection (which is about 4dp less than the spacing
that was previously being added).

Adds a compile-time flag to render the header at all times to
help visualize issues like this in the future. Check the
screenshots below to see some examples of this.

Screenshots: https://drive.google.com/drive/folders/1n4wwtjMwOR-eDIE1xAoG3oLXO7txpoT_?resourcekey=0-WP0RNbp7vFh2Yw1wUBZTaA&usp=sharing

Bug: 240672593
Bug: 243728487
Test: Manually checked search transition, scrolling behavior, and
highlight of the search results with and without a work profile.

Change-Id: I166d136e568c5621c81378fa09021666d95f0ffc
2022-09-23 19:36:44 -07:00

495 lines
17 KiB
Java

/*
* Copyright (C) 2017 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.allapps;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.views.ActivityContext;
import com.android.systemui.plugins.AllAppsRow;
import com.android.systemui.plugins.AllAppsRow.OnHeightUpdatedListener;
import com.android.systemui.plugins.PluginListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
public class FloatingHeaderView extends LinearLayout implements
ValueAnimator.AnimatorUpdateListener, PluginListener<AllAppsRow>, Insettable,
OnHeightUpdatedListener {
private final Rect mRVClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
private final Rect mHeaderClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
private final Point mTempOffset = new Point();
private final RecyclerView.OnScrollListener mOnScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {}
@Override
public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
if (rv != mCurrentRV) {
return;
}
if (mAnimator.isStarted()) {
mAnimator.cancel();
}
int current = -mCurrentRV.computeVerticalScrollOffset();
boolean headerCollapsed = mHeaderCollapsed;
moved(current);
applyVerticalMove();
if (headerCollapsed != mHeaderCollapsed) {
BaseAllAppsContainerView<?> parent =
(BaseAllAppsContainerView<?>) getParent();
parent.invalidateHeader();
}
}
};
protected final Map<AllAppsRow, PluginHeaderRow> mPluginRows = new ArrayMap<>();
// These two values are necessary to ensure that the header protection is drawn correctly.
private final int mTabsAdditionalPaddingTop;
private final int mTabsAdditionalPaddingBottom;
private boolean mHeaderProtectionSupported;
protected ViewGroup mTabLayout;
private AllAppsRecyclerView mMainRV;
private AllAppsRecyclerView mWorkRV;
private SearchRecyclerView mSearchRV;
private AllAppsRecyclerView mCurrentRV;
protected int mSnappedScrolledY;
private int mTranslationY;
private boolean mForwardToRecyclerView;
protected boolean mTabsHidden;
protected int mMaxTranslation;
// Whether the header has been scrolled off-screen.
private boolean mHeaderCollapsed;
// Whether floating rows like predicted apps are hidden.
private boolean mFloatingRowsCollapsed;
// Total height of all current floating rows. Collapsed rows == 0 height.
private int mFloatingRowsHeight;
// This is initialized once during inflation and stays constant after that. Fixed views
// cannot be added or removed dynamically.
private FloatingHeaderRow[] mFixedRows = FloatingHeaderRow.NO_ROWS;
// Array of all fixed rows and plugin rows. This is initialized every time a plugin is
// enabled or disabled, and represent the current set of all rows.
private FloatingHeaderRow[] mAllRows = FloatingHeaderRow.NO_ROWS;
public FloatingHeaderView(@NonNull Context context) {
this(context, null);
}
public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mTabsAdditionalPaddingTop = context.getResources()
.getDimensionPixelSize(R.dimen.all_apps_header_top_adjustment);
mTabsAdditionalPaddingBottom = context.getResources()
.getDimensionPixelSize(R.dimen.all_apps_header_bottom_adjustment);
mHeaderProtectionSupported = context.getResources().getBoolean(
R.bool.config_header_protection_supported);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mHeaderProtectionSupported = getContext().getResources().getBoolean(
R.bool.config_header_protection_supported);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTabLayout = findViewById(R.id.tabs);
// Find all floating header rows.
ArrayList<FloatingHeaderRow> rows = new ArrayList<>();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child instanceof FloatingHeaderRow) {
rows.add((FloatingHeaderRow) child);
}
}
mFixedRows = rows.toArray(new FloatingHeaderRow[rows.size()]);
mAllRows = mFixedRows;
updateFloatingRowsHeight();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(this,
AllAppsRow.class, true /* allowMultiple */);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
}
private void recreateAllRowsArray() {
int pluginCount = mPluginRows.size();
if (pluginCount == 0) {
mAllRows = mFixedRows;
} else {
int count = mFixedRows.length;
mAllRows = new FloatingHeaderRow[count + pluginCount];
for (int i = 0; i < count; i++) {
mAllRows[i] = mFixedRows[i];
}
for (PluginHeaderRow row : mPluginRows.values()) {
mAllRows[count] = row;
count++;
}
}
updateFloatingRowsHeight();
}
@Override
public void onPluginConnected(AllAppsRow allAppsRowPlugin, Context context) {
PluginHeaderRow headerRow = new PluginHeaderRow(allAppsRowPlugin, this);
addView(headerRow.mView, indexOfChild(mTabLayout));
mPluginRows.put(allAppsRowPlugin, headerRow);
recreateAllRowsArray();
allAppsRowPlugin.setOnHeightUpdatedListener(this);
}
@Override
public void onHeightUpdated() {
int oldMaxHeight = mMaxTranslation;
updateExpectedHeight();
if (mMaxTranslation != oldMaxHeight || mFloatingRowsCollapsed) {
BaseAllAppsContainerView<?> parent = (BaseAllAppsContainerView<?>) getParent();
if (parent != null) {
parent.setupHeader();
}
}
}
@Override
public void onPluginDisconnected(AllAppsRow plugin) {
PluginHeaderRow row = mPluginRows.get(plugin);
removeView(row.mView);
mPluginRows.remove(plugin);
recreateAllRowsArray();
onHeightUpdated();
}
@Override
public View getFocusedChild() {
if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
for (FloatingHeaderRow row : mAllRows) {
if (row.hasVisibleContent() && row.isVisible()) {
return row.getFocusedChild();
}
}
return null;
}
return super.getFocusedChild();
}
void setup(AllAppsRecyclerView mainRV, AllAppsRecyclerView workRV, SearchRecyclerView searchRV,
int activeRV, boolean tabsHidden) {
for (FloatingHeaderRow row : mAllRows) {
row.setup(this, mAllRows, tabsHidden);
}
updateExpectedHeight();
mTabsHidden = tabsHidden;
mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE);
mMainRV = mainRV;
mWorkRV = workRV;
mSearchRV = searchRV;
setActiveRV(activeRV);
reset(false);
}
/** Whether this header has been set up previously. */
boolean isSetUp() {
return mMainRV != null;
}
/** Set the active AllApps RV which will adjust the alpha of the header when scrolled. */
void setActiveRV(int rvType) {
if (mCurrentRV != null) {
mCurrentRV.removeOnScrollListener(mOnScrollListener);
}
mCurrentRV =
rvType == AdapterHolder.MAIN ? mMainRV
: rvType == AdapterHolder.WORK ? mWorkRV : mSearchRV;
mCurrentRV.addOnScrollListener(mOnScrollListener);
}
private void updateExpectedHeight() {
updateFloatingRowsHeight();
mMaxTranslation = 0;
if (mFloatingRowsCollapsed) {
return;
}
mMaxTranslation += mFloatingRowsHeight;
if (!mTabsHidden) {
mMaxTranslation += mTabsAdditionalPaddingBottom
+ getResources().getDimensionPixelSize(R.dimen.all_apps_tabs_margin_top);
}
}
int getMaxTranslation() {
if (mMaxTranslation == 0 && (mTabsHidden || mFloatingRowsCollapsed)) {
return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding);
} else if (mMaxTranslation > 0 && mTabsHidden) {
return mMaxTranslation + getPaddingTop();
} else {
return mMaxTranslation;
}
}
private boolean canSnapAt(int currentScrollY) {
return Math.abs(currentScrollY) <= mMaxTranslation;
}
private void moved(final int currentScrollY) {
if (mHeaderCollapsed) {
if (currentScrollY <= mSnappedScrolledY) {
if (canSnapAt(currentScrollY)) {
mSnappedScrolledY = currentScrollY;
}
} else {
mHeaderCollapsed = false;
}
mTranslationY = currentScrollY;
} else {
mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
// update state vars
if (mTranslationY >= 0) { // expanded: must not move down further
mTranslationY = 0;
mSnappedScrolledY = currentScrollY - mMaxTranslation;
} else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden
mHeaderCollapsed = true;
mSnappedScrolledY = -mMaxTranslation;
}
}
}
protected void applyVerticalMove() {
int uncappedTranslationY = mTranslationY;
mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
if (mFloatingRowsCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) {
// we hide it completely if already capped (for opening search anim)
for (FloatingHeaderRow row : mAllRows) {
row.setVerticalScroll(0, true /* isScrolledOut */);
}
} else {
for (FloatingHeaderRow row : mAllRows) {
row.setVerticalScroll(uncappedTranslationY, false /* isScrolledOut */);
}
}
mTabLayout.setTranslationY(mTranslationY);
int clipTop = getPaddingTop() - mTabsAdditionalPaddingTop;
if (mTabsHidden) {
// Add back spacing that is otherwise covered by the tabs.
clipTop += mTabsAdditionalPaddingTop;
}
mRVClip.top = mTabsHidden || mFloatingRowsCollapsed ? clipTop : 0;
mHeaderClip.top = clipTop;
// clipping on a draw might cause additional redraw
setClipBounds(mHeaderClip);
if (mMainRV != null) {
mMainRV.setClipBounds(mRVClip);
}
if (mWorkRV != null) {
mWorkRV.setClipBounds(mRVClip);
}
if (mSearchRV != null) {
mSearchRV.setClipBounds(mRVClip);
}
}
/**
* Hides all the floating rows
*/
public void setFloatingRowsCollapsed(boolean collapsed) {
if (mFloatingRowsCollapsed == collapsed) {
return;
}
mFloatingRowsCollapsed = collapsed;
onHeightUpdated();
}
public void reset(boolean animate) {
if (mAnimator.isStarted()) {
mAnimator.cancel();
}
if (animate) {
mAnimator.setIntValues(mTranslationY, 0);
mAnimator.addUpdateListener(this);
mAnimator.setDuration(150);
mAnimator.start();
} else {
mTranslationY = 0;
applyVerticalMove();
}
mHeaderCollapsed = false;
mSnappedScrolledY = -mMaxTranslation;
mCurrentRV.scrollToTop();
}
public boolean isExpanded() {
return !mHeaderCollapsed;
}
/** Returns true if personal/work tabs are currently in use. */
public boolean usingTabs() {
return !mTabsHidden;
}
ViewGroup getTabLayout() {
return mTabLayout;
}
/** Calculates the combined height of any floating rows (e.g. predicted apps, app divider). */
private void updateFloatingRowsHeight() {
mFloatingRowsHeight =
Arrays.stream(mAllRows).mapToInt(FloatingHeaderRow::getExpectedHeight).sum();
}
/** Gets the combined height of any floating rows (e.g. predicted apps, app divider). */
int getFloatingRowsHeight() {
return mFloatingRowsHeight;
}
int getTabsAdditionalPaddingTop() {
return mTabsAdditionalPaddingTop;
}
int getTabsAdditionalPaddingBottom() {
return mTabsAdditionalPaddingBottom;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mTranslationY = (Integer) animation.getAnimatedValue();
applyVerticalMove();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
calcOffset(mTempOffset);
ev.offsetLocation(mTempOffset.x, mTempOffset.y);
mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev);
ev.offsetLocation(-mTempOffset.x, -mTempOffset.y);
return mForwardToRecyclerView || super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mForwardToRecyclerView) {
// take this view's and parent view's (view pager) location into account
calcOffset(mTempOffset);
event.offsetLocation(mTempOffset.x, mTempOffset.y);
try {
return mCurrentRV.onTouchEvent(event);
} finally {
event.offsetLocation(-mTempOffset.x, -mTempOffset.y);
}
} else {
return super.onTouchEvent(event);
}
}
private void calcOffset(Point p) {
p.x = getLeft() - mCurrentRV.getLeft() - ((ViewGroup) mCurrentRV.getParent()).getLeft();
p.y = getTop() - mCurrentRV.getTop() - ((ViewGroup) mCurrentRV.getParent()).getTop();
}
public boolean isHeaderProtectionSupported() {
return mHeaderProtectionSupported;
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
@Override
public void setInsets(Rect insets) {
int leftRightPadding = ActivityContext.lookupContext(getContext())
.getDeviceProfile().allAppsLeftRightPadding;
setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
}
public <T extends FloatingHeaderRow> T findFixedRowByType(Class<T> type) {
for (FloatingHeaderRow row : mAllRows) {
if (row.getTypeClass() == type) {
return (T) row;
}
}
return null;
}
/**
* Returns visible height of FloatingHeaderView contents requiring header protection
*/
int getPeripheralProtectionHeight() {
if (!mHeaderProtectionSupported) {
return 0;
}
// we only want to show protection when work tab is available and header is either
// collapsed or animating to/from collapsed state
if (mTabsHidden || mFloatingRowsCollapsed || !mHeaderCollapsed) {
return 0;
}
return Math.max(0,
getTabLayout().getBottom() - getPaddingTop() + getPaddingBottom() + mTranslationY);
}
}