Files
Lawnchair/src/com/android/launcher3/allapps/FloatingHeaderView.java
T
Tony Wickham 35a4e0eec9 Tie prediction text to all apps interpolator
Now floating headers get 2 interpolators: one for the header content
itselt, and one for the all apps content that follows. That way, they
can choose to intperolate part of their content as if it were part of
all apps instead of the header. Currently, we do this to animate
predicted icons quickly, followed by the all apps icons, predictions
text, all apps scrollbar, and all apps divider as you continue swiping.

Bug: 132455160
Change-Id: Ib3e373c291e174e1306a53854d0ad4dc29eb4b76
2019-07-31 14:45:18 -07:00

413 lines
14 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.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.view.animation.Interpolator;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
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.Map;
public class FloatingHeaderView extends LinearLayout implements
ValueAnimator.AnimatorUpdateListener, PluginListener<AllAppsRow>, Insettable,
OnHeightUpdatedListener {
private final Rect mClip = 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(RecyclerView recyclerView, int newState) {
}
@Override
public void onScrolled(RecyclerView rv, int dx, int dy) {
if (rv != mCurrentRV) {
return;
}
if (mAnimator.isStarted()) {
mAnimator.cancel();
}
int current = -mCurrentRV.getCurrentScrollY();
moved(current);
applyVerticalMove();
}
};
private final int mHeaderTopPadding;
protected final Map<AllAppsRow, PluginHeaderRow> mPluginRows = new ArrayMap<>();
protected ViewGroup mTabLayout;
private AllAppsRecyclerView mMainRV;
private AllAppsRecyclerView mWorkRV;
private AllAppsRecyclerView mCurrentRV;
private ViewGroup mParent;
private boolean mHeaderCollapsed;
private int mSnappedScrolledY;
private int mTranslationY;
private boolean mAllowTouchForwarding;
private boolean mForwardToRecyclerView;
protected boolean mTabsHidden;
protected int mMaxTranslation;
private boolean mMainRVActive = true;
private boolean mCollapsed = false;
// 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);
mHeaderTopPadding = context.getResources()
.getDimensionPixelSize(R.dimen.all_apps_header_top_padding);
}
@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;
}
@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++;
}
}
}
@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) {
AllAppsContainerView parent = (AllAppsContainerView) 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();
}
public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) {
for (FloatingHeaderRow row : mAllRows) {
row.setup(this, mAllRows, tabsHidden);
}
updateExpectedHeight();
mTabsHidden = tabsHidden;
mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE);
mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView);
mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView);
mParent = (ViewGroup) mMainRV.getParent();
setMainActive(mMainRVActive || mWorkRV == null);
reset(false);
}
private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) {
if (old != updated && updated != null ) {
updated.addOnScrollListener(mOnScrollListener);
}
return updated;
}
private void updateExpectedHeight() {
mMaxTranslation = 0;
if (mCollapsed) {
return;
}
for (FloatingHeaderRow row : mAllRows) {
mMaxTranslation += row.getExpectedHeight();
}
}
public void setMainActive(boolean active) {
mCurrentRV = active ? mMainRV : mWorkRV;
mMainRVActive = active;
}
public int getMaxTranslation() {
if (mMaxTranslation == 0 && mTabsHidden) {
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 if (!mHeaderCollapsed) {
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 (mCollapsed || uncappedTranslationY < mTranslationY - mHeaderTopPadding) {
// 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);
mClip.top = mMaxTranslation + mTranslationY;
// clipping on a draw might cause additional redraw
mMainRV.setClipBounds(mClip);
if (mWorkRV != null) {
mWorkRV.setClipBounds(mClip);
}
}
/**
* Hides all the floating rows
*/
public void setCollapsed(boolean collapse) {
if (mCollapsed == collapse) return;
mCollapsed = collapse;
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;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mTranslationY = (Integer) animation.getAnimatedValue();
applyVerticalMove();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mAllowTouchForwarding) {
mForwardToRecyclerView = false;
return super.onInterceptTouchEvent(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() - mParent.getLeft();
p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
}
public void setContentVisibility(boolean hasHeader, boolean hasAllAppsContent,
PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
for (FloatingHeaderRow row : mAllRows) {
row.setContentVisibility(hasHeader, hasAllAppsContent, setter, headerFade, allAppsFade);
}
allowTouchForwarding(hasAllAppsContent);
setter.setFloat(mTabLayout, ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
}
protected void allowTouchForwarding(boolean allow) {
mAllowTouchForwarding = allow;
}
public boolean hasVisibleContent() {
for (FloatingHeaderRow row : mAllRows) {
if (row.hasVisibleContent()) {
return true;
}
}
return false;
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
@Override
public void setInsets(Rect insets) {
DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
for (FloatingHeaderRow row : mAllRows) {
row.setInsets(insets, grid);
}
}
public <T extends FloatingHeaderRow> T findFixedRowByType(Class<T> type) {
for (FloatingHeaderRow row : mAllRows) {
if (row.getTypeClass() == type) {
return (T) row;
}
}
return null;
}
}