/* * 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, 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 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 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 findFixedRowByType(Class type) { for (FloatingHeaderRow row : mAllRows) { if (row.getTypeClass() == type) { return (T) row; } } return null; } }