Merge "Support for animating A-Z <-> Search." into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
1c22b7e57c
@@ -20,6 +20,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/all_apps_header_pill_height"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:paddingTop="@dimen/all_apps_tabs_vertical_padding"
|
||||
android:paddingBottom="@dimen/all_apps_tabs_vertical_padding"
|
||||
android:orientation="horizontal"
|
||||
style="@style/TextHeadline">
|
||||
|
||||
@@ -28,7 +30,6 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/all_apps_tabs_button_horizontal_padding"
|
||||
android:layout_marginVertical="@dimen/all_apps_tabs_vertical_padding"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/all_apps_tabs_background"
|
||||
android:text="@string/all_apps_personal_tab"
|
||||
@@ -41,7 +42,6 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="@dimen/all_apps_tabs_button_horizontal_padding"
|
||||
android:layout_marginVertical="@dimen/all_apps_tabs_vertical_padding"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/all_apps_tabs_background"
|
||||
android:text="@string/all_apps_work_tab"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/search_results_list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:focusable="true" />
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
android:textColor="@color/all_apps_tab_text"
|
||||
android:textSize="14sp"
|
||||
android:background="@drawable/work_apps_toggle_background"
|
||||
android:forceHasOverlappingRendering="false"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableStart="@drawable/ic_corp_off"
|
||||
android:layout_marginBottom="@dimen/work_fab_margin_bottom"
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.android.launcher3.allapps;
|
||||
|
||||
import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.SEARCH;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
@@ -34,7 +36,6 @@ import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.views.AppLauncher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* All apps container view with search support for use in a dragging activity.
|
||||
@@ -44,6 +45,11 @@ import java.util.Objects;
|
||||
public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
& DeviceProfileListenable> extends BaseAllAppsContainerView<T> {
|
||||
|
||||
private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300;
|
||||
|
||||
// Used to animate Search results out and A-Z apps in, or vice-versa.
|
||||
private final SearchTransitionController mSearchTransitionController;
|
||||
|
||||
protected SearchUiManager mSearchUiManager;
|
||||
/**
|
||||
* View that defines the search box. Result is rendered inside the recycler view defined in the
|
||||
@@ -52,6 +58,7 @@ public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
private View mSearchContainer;
|
||||
/** {@code true} when rendered view is in search state instead of the scroll state. */
|
||||
private boolean mIsSearching;
|
||||
private boolean mRebindAdaptersAfterSearchAnimation;
|
||||
|
||||
public ActivityAllAppsContainerView(Context context) {
|
||||
this(context, null);
|
||||
@@ -63,6 +70,8 @@ public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
|
||||
public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
mSearchTransitionController = new SearchTransitionController(this);
|
||||
}
|
||||
|
||||
public SearchUiManager getSearchUiManager() {
|
||||
@@ -73,19 +82,10 @@ public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
return mSearchContainer;
|
||||
}
|
||||
|
||||
/** Updates all apps container with the latest search query. */
|
||||
public void setLastSearchQuery(String query) {
|
||||
mIsSearching = true;
|
||||
rebindAdapters();
|
||||
mHeader.setCollapsed(true);
|
||||
}
|
||||
|
||||
/** Invoke when the current search session is finished. */
|
||||
public void onClearSearchResult() {
|
||||
mIsSearching = false;
|
||||
mHeader.setCollapsed(false);
|
||||
animateToSearchState(false);
|
||||
rebindAdapters();
|
||||
mHeader.reset(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,12 +93,42 @@ public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
*/
|
||||
public void setSearchResults(ArrayList<AdapterItem> results) {
|
||||
if (getSearchResultList().setSearchResults(results)) {
|
||||
for (int i = 0; i < mAH.size(); i++) {
|
||||
if (mAH.get(i).mRecyclerView != null) {
|
||||
mAH.get(i).mRecyclerView.onSearchResultsChanged();
|
||||
}
|
||||
}
|
||||
getSearchRecyclerView().onSearchResultsChanged();
|
||||
}
|
||||
if (results != null) {
|
||||
animateToSearchState(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void animateToSearchState(boolean goingToSearch) {
|
||||
animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS);
|
||||
}
|
||||
|
||||
private void animateToSearchState(boolean goingToSearch, long durationMs) {
|
||||
if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) {
|
||||
return;
|
||||
}
|
||||
if (goingToSearch) {
|
||||
// Fade out the button to pause work apps.
|
||||
mWorkManager.onActivePageChanged(SEARCH);
|
||||
}
|
||||
mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
|
||||
/* onEndRunnable = */ () -> {
|
||||
mIsSearching = goingToSearch;
|
||||
updateSearchResultsVisibility();
|
||||
int previousPage = getCurrentPage();
|
||||
if (mRebindAdaptersAfterSearchAnimation) {
|
||||
rebindAdapters(false);
|
||||
mRebindAdaptersAfterSearchAnimation = false;
|
||||
}
|
||||
if (!goingToSearch) {
|
||||
setSearchResults(null);
|
||||
if (mViewPager != null) {
|
||||
mViewPager.setCurrentPage(previousPage);
|
||||
}
|
||||
onActivePageChanged(previousPage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,6 +151,8 @@ public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
super.reset(animate);
|
||||
// Reset the search bar after transitioning home.
|
||||
mSearchUiManager.resetSearch();
|
||||
// Animate to A-Z with 0 time to reset the animation with proper state management.
|
||||
animateToSearchState(false, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,22 +188,31 @@ public class ActivityAllAppsContainerView<T extends Context & AppLauncher
|
||||
return mIsSearching;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivePageChanged(int currentActivePage) {
|
||||
if (mSearchTransitionController.isRunning()) {
|
||||
// Will be called at the end of the animation.
|
||||
return;
|
||||
}
|
||||
super.onActivePageChanged(currentActivePage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rebindAdapters(boolean force) {
|
||||
if (mSearchTransitionController.isRunning()) {
|
||||
mRebindAdaptersAfterSearchAnimation = true;
|
||||
return;
|
||||
}
|
||||
super.rebindAdapters(force);
|
||||
if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()
|
||||
|| getMainAdapterProvider().getDecorator() == null) {
|
||||
|| getMainAdapterProvider().getDecorator() == null
|
||||
|| getSearchRecyclerView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator();
|
||||
mAH.stream()
|
||||
.map(adapterHolder -> adapterHolder.mRecyclerView)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(v -> {
|
||||
v.removeItemDecoration(decoration); // Remove in case it is already added.
|
||||
v.addItemDecoration(decoration);
|
||||
});
|
||||
getSearchRecyclerView().removeItemDecoration(decoration); // In case it is already added.
|
||||
getSearchRecyclerView().addItemDecoration(decoration);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -127,6 +127,11 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> ex
|
||||
public boolean isContentSame(AdapterItem other) {
|
||||
return itemInfo == null && other.itemInfo == null;
|
||||
}
|
||||
|
||||
/** Sets the alpha of the decorator for this item. Returns true if successful. */
|
||||
public boolean setDecorationFillAlpha(int alpha) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected final T mActivityContext;
|
||||
|
||||
@@ -107,12 +107,13 @@ public abstract class BaseAllAppsContainerView<T extends Context & ActivityConte
|
||||
updateHeaderScroll(((AllAppsRecyclerView) recyclerView).getCurrentScrollY());
|
||||
}
|
||||
};
|
||||
private final WorkProfileManager mWorkManager;
|
||||
|
||||
protected final WorkProfileManager mWorkManager;
|
||||
|
||||
private final Paint mNavBarScrimPaint;
|
||||
private int mNavBarScrimHeight = 0;
|
||||
|
||||
private AllAppsPagedView mViewPager;
|
||||
protected AllAppsPagedView mViewPager;
|
||||
private SearchRecyclerView mSearchRecyclerView;
|
||||
|
||||
protected FloatingHeaderView mHeader;
|
||||
@@ -349,7 +350,7 @@ public abstract class BaseAllAppsContainerView<T extends Context & ActivityConte
|
||||
* The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently
|
||||
* hidden while searching.
|
||||
**/
|
||||
private View getAppsRecyclerViewContainer() {
|
||||
protected View getAppsRecyclerViewContainer() {
|
||||
return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
|
||||
}
|
||||
|
||||
@@ -527,7 +528,7 @@ public abstract class BaseAllAppsContainerView<T extends Context & ActivityConte
|
||||
mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
|
||||
}
|
||||
|
||||
private void updateSearchResultsVisibility() {
|
||||
protected void updateSearchResultsVisibility() {
|
||||
if (isSearching()) {
|
||||
getSearchRecyclerView().setVisibility(VISIBLE);
|
||||
getAppsRecyclerViewContainer().setVisibility(GONE);
|
||||
|
||||
@@ -42,6 +42,7 @@ 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
|
||||
@@ -82,8 +83,8 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
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 mHeaderTopAdjustment;
|
||||
private final int mHeaderBottomAdjustment;
|
||||
private final int mTabsAdditionalPaddingTop;
|
||||
private final int mTabsAdditionalPaddingBottom;
|
||||
private boolean mHeaderProtectionSupported;
|
||||
|
||||
protected ViewGroup mTabLayout;
|
||||
@@ -91,7 +92,6 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
private AllAppsRecyclerView mWorkRV;
|
||||
private SearchRecyclerView mSearchRV;
|
||||
private AllAppsRecyclerView mCurrentRV;
|
||||
public boolean mHeaderCollapsed;
|
||||
protected int mSnappedScrolledY;
|
||||
private int mTranslationY;
|
||||
|
||||
@@ -100,7 +100,12 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
protected boolean mTabsHidden;
|
||||
protected int mMaxTranslation;
|
||||
|
||||
private boolean mCollapsed = false;
|
||||
// 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.
|
||||
@@ -117,9 +122,9 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
|
||||
public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mHeaderTopAdjustment = context.getResources()
|
||||
mTabsAdditionalPaddingTop = context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.all_apps_header_top_adjustment);
|
||||
mHeaderBottomAdjustment = context.getResources()
|
||||
mTabsAdditionalPaddingBottom = context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.all_apps_header_bottom_adjustment);
|
||||
mHeaderProtectionSupported = context.getResources().getBoolean(
|
||||
R.bool.config_header_protection_supported);
|
||||
@@ -148,6 +153,7 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
}
|
||||
mFixedRows = rows.toArray(new FloatingHeaderRow[rows.size()]);
|
||||
mAllRows = mFixedRows;
|
||||
updateFloatingRowsHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,6 +185,7 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
count++;
|
||||
}
|
||||
}
|
||||
updateFloatingRowsHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -195,7 +202,7 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
int oldMaxHeight = mMaxTranslation;
|
||||
updateExpectedHeight();
|
||||
|
||||
if (mMaxTranslation != oldMaxHeight || mCollapsed) {
|
||||
if (mMaxTranslation != oldMaxHeight || mFloatingRowsCollapsed) {
|
||||
BaseAllAppsContainerView<?> parent = (BaseAllAppsContainerView<?>) getParent();
|
||||
if (parent != null) {
|
||||
parent.setupHeader();
|
||||
@@ -258,20 +265,19 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
}
|
||||
|
||||
private void updateExpectedHeight() {
|
||||
updateFloatingRowsHeight();
|
||||
mMaxTranslation = 0;
|
||||
if (mCollapsed) {
|
||||
if (mFloatingRowsCollapsed) {
|
||||
return;
|
||||
}
|
||||
for (FloatingHeaderRow row : mAllRows) {
|
||||
mMaxTranslation += row.getExpectedHeight();
|
||||
}
|
||||
mMaxTranslation += mFloatingRowsHeight;
|
||||
if (!mTabsHidden) {
|
||||
mMaxTranslation += mHeaderBottomAdjustment;
|
||||
mMaxTranslation += mTabsAdditionalPaddingBottom;
|
||||
}
|
||||
}
|
||||
|
||||
public int getMaxTranslation() {
|
||||
if (mMaxTranslation == 0 && mTabsHidden) {
|
||||
if (mMaxTranslation == 0 && (mTabsHidden || mFloatingRowsCollapsed)) {
|
||||
return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding);
|
||||
} else if (mMaxTranslation > 0 && mTabsHidden) {
|
||||
return mMaxTranslation + getPaddingTop();
|
||||
@@ -312,7 +318,7 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
int uncappedTranslationY = mTranslationY;
|
||||
mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
|
||||
|
||||
if (mCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) {
|
||||
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 */);
|
||||
@@ -325,11 +331,11 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
|
||||
mTabLayout.setTranslationY(mTranslationY);
|
||||
|
||||
int clipTop = getPaddingTop() - mHeaderTopAdjustment;
|
||||
int clipTop = getPaddingTop() - mTabsAdditionalPaddingTop;
|
||||
if (mTabsHidden) {
|
||||
clipTop += getPaddingBottom() - mHeaderBottomAdjustment;
|
||||
clipTop += getPaddingBottom() - mTabsAdditionalPaddingBottom;
|
||||
}
|
||||
mRVClip.top = mTabsHidden ? clipTop : 0;
|
||||
mRVClip.top = mTabsHidden || mFloatingRowsCollapsed ? clipTop : 0;
|
||||
mHeaderClip.top = clipTop;
|
||||
// clipping on a draw might cause additional redraw
|
||||
setClipBounds(mHeaderClip);
|
||||
@@ -347,10 +353,12 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
/**
|
||||
* Hides all the floating rows
|
||||
*/
|
||||
public void setCollapsed(boolean collapse) {
|
||||
if (mCollapsed == collapse) return;
|
||||
public void setFloatingRowsCollapsed(boolean collapsed) {
|
||||
if (mFloatingRowsCollapsed == collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCollapsed = collapse;
|
||||
mFloatingRowsCollapsed = collapsed;
|
||||
onHeightUpdated();
|
||||
}
|
||||
|
||||
@@ -376,6 +384,30 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
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 getTabsAdditionalPaddingBottom() {
|
||||
return mTabsAdditionalPaddingBottom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
mTranslationY = (Integer) animation.getAnimatedValue();
|
||||
@@ -447,9 +479,10 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
|
||||
// we only want to show protection when work tab is available and header is either
|
||||
// collapsed or animating to/from collapsed state
|
||||
if (mTabsHidden || !mHeaderCollapsed) {
|
||||
if (mTabsHidden || mFloatingRowsCollapsed || !mHeaderCollapsed) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(getHeight() - getPaddingTop() + mTranslationY, 0);
|
||||
return Math.max(0,
|
||||
getTabLayout().getBottom() - getPaddingTop() + getPaddingBottom() + mTranslationY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,17 @@ package com.android.launcher3.allapps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.android.launcher3.views.RecyclerViewFastScroller;
|
||||
|
||||
/** A RecyclerView for AllApps Search results. */
|
||||
public class SearchRecyclerView extends AllAppsRecyclerView {
|
||||
private static final String TAG = "SearchRecyclerView";
|
||||
|
||||
private Consumer<View> mChildAttachedConsumer;
|
||||
|
||||
public SearchRecyclerView(Context context) {
|
||||
this(context, null);
|
||||
@@ -41,6 +46,11 @@ public class SearchRecyclerView extends AllAppsRecyclerView {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/** This will be called just before a new child is attached to the window. */
|
||||
public void setChildAttachedConsumer(Consumer<View> childAttachedConsumer) {
|
||||
mChildAttachedConsumer = childAttachedConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updatePoolSize() {
|
||||
RecycledViewPool pool = getRecycledViewPool();
|
||||
@@ -57,4 +67,12 @@ public class SearchRecyclerView extends AllAppsRecyclerView {
|
||||
public RecyclerViewFastScroller getScrollbar() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildAttachedToWindow(@NonNull View child) {
|
||||
if (mChildAttachedConsumer != null) {
|
||||
mChildAttachedConsumer.accept(child);
|
||||
}
|
||||
super.onChildAttachedToWindow(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 static android.view.View.VISIBLE;
|
||||
|
||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
|
||||
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
|
||||
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
|
||||
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
|
||||
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
||||
import static com.android.launcher3.anim.Interpolators.clampToProgress;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.FloatProperty;
|
||||
import android.view.View;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import com.android.launcher3.BubbleTextView;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherState;
|
||||
|
||||
/** Coordinates the transition between Search and A-Z in All Apps. */
|
||||
public class SearchTransitionController {
|
||||
|
||||
// Interpolator when the user taps the QSB while already in All Apps.
|
||||
private static final Interpolator DEFAULT_INTERPOLATOR_WITHIN_ALL_APPS = DEACCEL_1_7;
|
||||
// Interpolator when the user taps the QSB from home screen, so transition to all apps is
|
||||
// happening simultaneously.
|
||||
private static final Interpolator DEFAULT_INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = LINEAR;
|
||||
|
||||
/**
|
||||
* These values represent points on the [0, 1] animation progress spectrum. They are used to
|
||||
* animate items in the {@link SearchRecyclerView}.
|
||||
*/
|
||||
private static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
|
||||
private static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
|
||||
private static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
|
||||
private static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
|
||||
private static final float CONTENT_STAGGER = 0.01f; // Progress before next item starts fading.
|
||||
|
||||
private static final FloatProperty<SearchTransitionController> SEARCH_TO_AZ_PROGRESS =
|
||||
new FloatProperty<SearchTransitionController>("searchToAzProgress") {
|
||||
@Override
|
||||
public Float get(SearchTransitionController controller) {
|
||||
return controller.getSearchToAzProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(SearchTransitionController controller, float progress) {
|
||||
controller.setSearchToAzProgress(progress);
|
||||
}
|
||||
};
|
||||
|
||||
private final ActivityAllAppsContainerView<?> mAllAppsContainerView;
|
||||
|
||||
private ObjectAnimator mSearchToAzAnimator = null;
|
||||
private float mSearchToAzProgress = 1f;
|
||||
|
||||
public SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView) {
|
||||
mAllAppsContainerView = allAppsContainerView;
|
||||
}
|
||||
|
||||
/** Returns true if a transition animation is currently in progress. */
|
||||
public boolean isRunning() {
|
||||
return mSearchToAzAnimator != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the transition to or from search state. If a transition is already in progress, the
|
||||
* animation will start from that point with the new duration, and the previous onEndRunnable
|
||||
* will not be called.
|
||||
*
|
||||
* @param goingToSearch true if will be showing search results, otherwise will be showing a-z
|
||||
* @param duration time in ms for the animation to run
|
||||
* @param onEndRunnable will be called when the animation finishes, unless another animation is
|
||||
* scheduled in the meantime
|
||||
*/
|
||||
public void animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
|
||||
float targetProgress = goingToSearch ? 0 : 1;
|
||||
|
||||
if (mSearchToAzAnimator != null) {
|
||||
mSearchToAzAnimator.cancel();
|
||||
}
|
||||
|
||||
mSearchToAzAnimator = ObjectAnimator.ofFloat(this, SEARCH_TO_AZ_PROGRESS, targetProgress);
|
||||
boolean inAllApps = Launcher.getLauncher(
|
||||
mAllAppsContainerView.getContext()).getStateManager().isInStableState(
|
||||
LauncherState.ALL_APPS);
|
||||
mSearchToAzAnimator.setDuration(duration).setInterpolator(
|
||||
inAllApps ? DEFAULT_INTERPOLATOR_WITHIN_ALL_APPS
|
||||
: DEFAULT_INTERPOLATOR_TRANSITIONING_TO_ALL_APPS);
|
||||
mSearchToAzAnimator.addListener(forEndCallback(() -> mSearchToAzAnimator = null));
|
||||
if (!goingToSearch) {
|
||||
mSearchToAzAnimator.addListener(forSuccessCallback(() -> {
|
||||
mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(false);
|
||||
mAllAppsContainerView.getFloatingHeaderView().reset(false /* animate */);
|
||||
mAllAppsContainerView.getAppsRecyclerViewContainer().setTranslationY(0);
|
||||
}));
|
||||
}
|
||||
mSearchToAzAnimator.addListener(forSuccessCallback(onEndRunnable));
|
||||
|
||||
mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(true);
|
||||
mAllAppsContainerView.getAppsRecyclerViewContainer().setVisibility(VISIBLE);
|
||||
getSearchRecyclerView().setVisibility(VISIBLE);
|
||||
getSearchRecyclerView().setChildAttachedConsumer(this::onSearchChildAttached);
|
||||
mSearchToAzAnimator.start();
|
||||
}
|
||||
|
||||
private SearchRecyclerView getSearchRecyclerView() {
|
||||
return mAllAppsContainerView.getSearchRecyclerView();
|
||||
}
|
||||
|
||||
private void setSearchToAzProgress(float searchToAzProgress) {
|
||||
mSearchToAzProgress = searchToAzProgress;
|
||||
int searchHeight = updateSearchRecyclerViewProgress();
|
||||
|
||||
FloatingHeaderView headerView = mAllAppsContainerView.getFloatingHeaderView();
|
||||
|
||||
// Add predictions + app divider height to account for predicted apps which will now be in
|
||||
// the Search RV instead of the floating header view. Note `getFloatingRowsHeight` returns 0
|
||||
// when predictions are not shown.
|
||||
int appsTranslationY = searchHeight + headerView.getFloatingRowsHeight();
|
||||
|
||||
if (headerView.usingTabs()) {
|
||||
// Move tabs below the search results, and fade them out in 20% of the animation.
|
||||
headerView.setTranslationY(searchHeight);
|
||||
headerView.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
|
||||
|
||||
// Account for the additional padding added for the tabs.
|
||||
appsTranslationY -=
|
||||
headerView.getPaddingTop() - headerView.getTabsAdditionalPaddingBottom();
|
||||
}
|
||||
|
||||
View appsContainer = mAllAppsContainerView.getAppsRecyclerViewContainer();
|
||||
appsContainer.setTranslationY(appsTranslationY);
|
||||
// Fade apps out with tabs (in 20% of the total animation).
|
||||
appsContainer.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the children views of SearchRecyclerView based on the current animation progress.
|
||||
*
|
||||
* @return the total height of animating views (excluding any app icons).
|
||||
*/
|
||||
private int updateSearchRecyclerViewProgress() {
|
||||
int numSearchResultsAnimated = 0;
|
||||
int totalHeight = 0;
|
||||
int appRowHeight = 0;
|
||||
Integer top = null;
|
||||
SearchRecyclerView searchRecyclerView = getSearchRecyclerView();
|
||||
for (int i = 0; i < searchRecyclerView.getChildCount(); i++) {
|
||||
View searchResultView = searchRecyclerView.getChildAt(i);
|
||||
if (searchResultView == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (top == null) {
|
||||
top = searchResultView.getTop();
|
||||
}
|
||||
|
||||
if (searchResultView instanceof BubbleTextView) {
|
||||
// The first app icon will set appRowHeight, which will also contribute to
|
||||
// totalHeight. Additional app icons should remove the appRowHeight to remain in
|
||||
// the same row as the first app.
|
||||
searchResultView.setY(top + totalHeight - appRowHeight);
|
||||
if (appRowHeight == 0) {
|
||||
appRowHeight = searchResultView.getHeight();
|
||||
totalHeight += appRowHeight;
|
||||
}
|
||||
// Don't scale/fade app row.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust content alpha based on start progress and stagger.
|
||||
float startContentFadeProgress = Math.max(0,
|
||||
TOP_CONTENT_FADE_PROGRESS_START - CONTENT_STAGGER * numSearchResultsAnimated);
|
||||
float endContentFadeProgress = Math.min(1,
|
||||
startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
|
||||
searchResultView.setAlpha(1 - clampToProgress(mSearchToAzProgress,
|
||||
startContentFadeProgress, endContentFadeProgress));
|
||||
|
||||
// Adjust background (or decorator) alpha based on start progress and stagger.
|
||||
float startBackgroundFadeProgress = Math.max(0,
|
||||
TOP_BACKGROUND_FADE_PROGRESS_START
|
||||
- CONTENT_STAGGER * numSearchResultsAnimated);
|
||||
float endBackgroundFadeProgress = Math.min(1,
|
||||
startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
|
||||
float backgroundAlpha = 1 - clampToProgress(mSearchToAzProgress,
|
||||
startBackgroundFadeProgress, endBackgroundFadeProgress);
|
||||
int adapterPosition = searchRecyclerView.getChildAdapterPosition(searchResultView);
|
||||
boolean decoratorFilled =
|
||||
adapterPosition != NO_POSITION
|
||||
&& searchRecyclerView.getApps().getAdapterItems().get(adapterPosition)
|
||||
.setDecorationFillAlpha((int) (255 * backgroundAlpha));
|
||||
if (!decoratorFilled) {
|
||||
// Try to adjust background alpha instead (e.g. for Search Edu card).
|
||||
Drawable background = searchResultView.getBackground();
|
||||
if (background != null) {
|
||||
background.setAlpha((int) (255 * backgroundAlpha));
|
||||
}
|
||||
}
|
||||
|
||||
float scaleY = 1 - mSearchToAzProgress;
|
||||
int scaledHeight = (int) (searchResultView.getHeight() * scaleY);
|
||||
searchResultView.setScaleY(scaleY);
|
||||
searchResultView.setY(top + totalHeight);
|
||||
|
||||
numSearchResultsAnimated++;
|
||||
totalHeight += scaledHeight;
|
||||
}
|
||||
|
||||
return totalHeight - appRowHeight;
|
||||
}
|
||||
|
||||
/** Called just before a child is attached to the SearchRecyclerView. */
|
||||
private void onSearchChildAttached(View child) {
|
||||
// Avoid allocating hardware layers for alpha changes.
|
||||
child.forceHasOverlappingRendering(false);
|
||||
if (mSearchToAzProgress > 0) {
|
||||
// Before the child is rendered, apply the animation including it to avoid flicker.
|
||||
updateSearchRecyclerViewProgress();
|
||||
} else {
|
||||
// Apply default states without processing the full layout.
|
||||
child.setAlpha(1);
|
||||
child.setScaleY(1);
|
||||
child.setTranslationY(0);
|
||||
int adapterPosition = getSearchRecyclerView().getChildAdapterPosition(child);
|
||||
if (adapterPosition != NO_POSITION) {
|
||||
getSearchRecyclerView().getApps().getAdapterItems().get(adapterPosition)
|
||||
.setDecorationFillAlpha(255);
|
||||
}
|
||||
if (child.getBackground() != null) {
|
||||
child.getBackground().setAlpha(255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float getSearchToAzProgress() {
|
||||
return mSearchToAzProgress;
|
||||
}
|
||||
}
|
||||
@@ -168,14 +168,11 @@ public class AppsSearchContainerLayout extends ExtendedEditText
|
||||
public void onSearchResult(String query, ArrayList<AdapterItem> items) {
|
||||
if (items != null) {
|
||||
mAppsView.setSearchResults(items);
|
||||
mAppsView.setLastSearchQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSearchResult() {
|
||||
mAppsView.setSearchResults(null);
|
||||
|
||||
// Clear the search query
|
||||
mSearchQueryBuilder.clear();
|
||||
mSearchQueryBuilder.clearSpans();
|
||||
|
||||
@@ -22,6 +22,7 @@ import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
@@ -135,7 +136,11 @@ public class WorkProfileTest extends AbstractLauncherUiTest {
|
||||
LauncherInstrumentation.WAIT_TIME_MS);
|
||||
|
||||
//start work profile toggle OFF test
|
||||
executeOnLauncher(l -> l.getAppsView().getWorkManager().getWorkModeSwitch().performClick());
|
||||
executeOnLauncher(l -> {
|
||||
// Ensure updates are not deferred so notification happens when apps pause.
|
||||
l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
|
||||
l.getAppsView().getWorkManager().getWorkModeSwitch().performClick();
|
||||
});
|
||||
|
||||
waitForLauncherCondition("Work profile toggle OFF failed", launcher -> {
|
||||
manager.reset(); // pulls current state from system
|
||||
|
||||
Reference in New Issue
Block a user