blob: 463c79c6696a823aafe496103e8a23da0d502794 [file] [log] [blame]
/*
* Copyright (C) 2016 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.systemui.qs;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.View.OnLayoutChangeListener;
import androidx.annotation.Nullable;
import com.android.app.animation.Interpolators;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.QSPanel.QSTileLayout;
import com.android.systemui.qs.TouchAnimator.Builder;
import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.qs.tileimpl.HeightOverrideable;
import com.android.systemui.tuner.TunerService;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* Performs the animated transition between the QQS and QS views.
*
* <p>The transition is driven externally via {@link #setPosition(float)}, where 0 is a fully
* collapsed QQS and one a fully expanded QS.
*
* <p>This implementation maintains a set of {@code TouchAnimator} to transition the properties of
* views both in QQS and QS. These {@code TouchAnimator} are re-created lazily if contents of either
* view change, see {@link #requestAnimatorUpdate()}.
*
* <p>During the transition, both QS and QQS are visible. For overlapping tiles (Whenever the QS
* shows the first page), the corresponding QS tiles are hidden until QS is fully expanded.
*/
@QSScope
public class QSAnimator implements QSHost.Callback, PagedTileLayout.PageListener,
TouchAnimator.Listener, OnLayoutChangeListener,
OnAttachStateChangeListener {
private static final String TAG = "QSAnimator";
private static final float EXPANDED_TILE_DELAY = .86f;
//Non first page delays
private static final float QS_TILE_LABEL_FADE_OUT_START = 0.15f;
private static final float QS_TILE_LABEL_FADE_OUT_END = 0.7f;
private static final float QQS_FADE_IN_INTERVAL = 0.1f;
public static final float SHORT_PARALLAX_AMOUNT = 0.1f;
/**
* List of all views that will be reset when clearing animation state
* see {@link #clearAnimationState()} }
*/
private final ArrayList<View> mAllViews = new ArrayList<>();
/**
* List of {@link View}s representing Quick Settings that are being animated from the quick QS
* position to the normal QS panel. These views will only show once the animation is complete,
* to prevent overlapping of semi transparent views
*/
private final ArrayList<View> mAnimatedQsViews = new ArrayList<>();
private final QuickQSPanel mQuickQsPanel;
private final QSPanelController mQsPanelController;
private final QuickQSPanelController mQuickQSPanelController;
private final QuickStatusBarHeader mQuickStatusBarHeader;
private final QS mQs;
@Nullable
private PagedTileLayout mPagedLayout;
private boolean mOnFirstPage = true;
private int mCurrentPage = 0;
private final QSExpansionPathInterpolator mQSExpansionPathInterpolator;
// Animator for elements in the first page, including secondary labels and qqs brightness
// slider, as well as animating the alpha of the QS tile layout (as we are tracking QQS tiles)
@Nullable
private TouchAnimator mFirstPageAnimator;
// TranslationX animator for QQS/QS tiles. Only used on the first page!
private TouchAnimator mTranslationXAnimator;
// TranslationY animator for QS tiles (and their components) in the first page
private TouchAnimator mTranslationYAnimator;
// TranslationY animator for QQS tiles (and their components)
private TouchAnimator mQQSTranslationYAnimator;
// Animates alpha of permanent views (QS tile layout, QQS tiles) when not in first page
private TouchAnimator mNonfirstPageAlphaAnimator;
// This animates fading of media player
private TouchAnimator mAllPagesDelayedAnimator;
// Brightness slider translation driver, uses mQSExpansionPathInterpolator.yInterpolator
@Nullable
private TouchAnimator mBrightnessTranslationAnimator;
// Brightness slider opacity driver. Uses linear interpolator.
@Nullable
private TouchAnimator mBrightnessOpacityAnimator;
// Animator for Footer actions in QQS
private TouchAnimator mQQSFooterActionsAnimator;
// Height animator for QQS tiles (height changing from QQS size to QS size)
@Nullable
private HeightExpansionAnimator mQQSTileHeightAnimator;
// Height animator for QS tile in first page but not in QQS, to present the illusion that they
// are expanding alongside the QQS tiles
@Nullable
private HeightExpansionAnimator mOtherFirstPageTilesHeightAnimator;
// Pair of animators for each non first page. The creation is delayed until the user first
// scrolls to that page, in order to get the proper measures and layout.
private final SparseArray<Pair<HeightExpansionAnimator, TouchAnimator>>
mNonFirstPageQSAnimators = new SparseArray<>();
private boolean mNeedsAnimatorUpdate = false;
private boolean mOnKeyguard;
private int mNumQuickTiles;
private int mLastQQSTileHeight;
private float mLastPosition;
private final QSHost mHost;
private final Executor mExecutor;
private boolean mShowCollapsedOnKeyguard;
private int mQQSTop;
private int[] mTmpLoc1 = new int[2];
private int[] mTmpLoc2 = new int[2];
@Inject
public QSAnimator(QS qs, QuickQSPanel quickPanel, QuickStatusBarHeader quickStatusBarHeader,
QSPanelController qsPanelController,
QuickQSPanelController quickQSPanelController, QSHost qsTileHost,
@Main Executor executor, TunerService tunerService,
QSExpansionPathInterpolator qsExpansionPathInterpolator) {
mQs = qs;
mQuickQsPanel = quickPanel;
mQsPanelController = qsPanelController;
mQuickQSPanelController = quickQSPanelController;
mQuickStatusBarHeader = quickStatusBarHeader;
mHost = qsTileHost;
mExecutor = executor;
mQSExpansionPathInterpolator = qsExpansionPathInterpolator;
mHost.addCallback(this);
mQsPanelController.addOnAttachStateChangeListener(this);
qs.getView().addOnLayoutChangeListener(this);
if (mQsPanelController.isAttachedToWindow()) {
onViewAttachedToWindow(null);
}
QSTileLayout tileLayout = mQsPanelController.getTileLayout();
if (tileLayout instanceof PagedTileLayout) {
mPagedLayout = ((PagedTileLayout) tileLayout);
} else {
Log.w(TAG, "QS Not using page layout");
}
mQsPanelController.setPageListener(this);
}
public void onRtlChanged() {
updateAnimators();
}
/**
* Request an update to the animators. This will update them lazily next time the position
* is changed.
*/
public void requestAnimatorUpdate() {
mNeedsAnimatorUpdate = true;
}
public void setOnKeyguard(boolean onKeyguard) {
mOnKeyguard = onKeyguard;
updateQQSVisibility();
if (mOnKeyguard) {
clearAnimationState();
}
}
/**
* Sets whether or not the keyguard is currently being shown with a collapsed header.
*/
void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) {
mShowCollapsedOnKeyguard = showCollapsedOnKeyguard;
updateQQSVisibility();
setCurrentPosition();
}
private void setCurrentPosition() {
setPosition(mLastPosition);
}
private void updateQQSVisibility() {
mQuickQsPanel.setVisibility(mOnKeyguard
&& !mShowCollapsedOnKeyguard ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void onViewAttachedToWindow(@NonNull View view) {
updateAnimators();
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) {
mHost.removeCallback(this);
}
private void addNonFirstPageAnimators(int page) {
Pair<HeightExpansionAnimator, TouchAnimator> pair = createSecondaryPageAnimators(page);
if (pair != null) {
// pair is null in one of two cases:
// * mPagedTileLayout is null, meaning we are still setting up.
// * the page has no tiles
// In either case, don't add the animators to the map.
mNonFirstPageQSAnimators.put(page, pair);
}
}
@Override
public void onPageChanged(boolean isFirst, int currentPage) {
if (currentPage != INVALID_PAGE && mCurrentPage != currentPage) {
mCurrentPage = currentPage;
if (!isFirst && !mNonFirstPageQSAnimators.contains(currentPage)) {
addNonFirstPageAnimators(currentPage);
}
}
if (mOnFirstPage == isFirst) return;
if (!isFirst) {
clearAnimationState();
}
mOnFirstPage = isFirst;
}
private void translateContent(
View qqsView,
View qsView,
View commonParent,
int xOffset,
int yOffset,
int[] temp,
TouchAnimator.Builder animatorBuilderX,
TouchAnimator.Builder animatorBuilderY,
TouchAnimator.Builder qqsAnimatorBuilderY
) {
getRelativePosition(temp, qqsView, commonParent);
int qqsPosX = temp[0];
int qqsPosY = temp[1];
getRelativePosition(temp, qsView, commonParent);
int qsPosX = temp[0];
int qsPosY = temp[1];
int xDiff = qsPosX - qqsPosX - xOffset;
animatorBuilderX.addFloat(qqsView, "translationX", 0, xDiff);
animatorBuilderX.addFloat(qsView, "translationX", -xDiff, 0);
int yDiff = qsPosY - qqsPosY - yOffset;
qqsAnimatorBuilderY.addFloat(qqsView, "translationY", 0, yDiff);
animatorBuilderY.addFloat(qsView, "translationY", -yDiff, 0);
mAllViews.add(qqsView);
mAllViews.add(qsView);
}
private void updateAnimators() {
mNeedsAnimatorUpdate = false;
TouchAnimator.Builder firstPageBuilder = new Builder();
TouchAnimator.Builder translationYBuilder = new Builder();
TouchAnimator.Builder qqsTranslationYBuilder = new Builder();
TouchAnimator.Builder translationXBuilder = new Builder();
TouchAnimator.Builder nonFirstPageAlphaBuilder = new Builder();
TouchAnimator.Builder quadraticInterpolatorBuilder = new Builder()
.setInterpolator(Interpolators.ACCELERATE);
Collection<QSTile> tiles = mHost.getTiles();
int count = 0;
clearAnimationState();
mNonFirstPageQSAnimators.clear();
mAllViews.clear();
mAnimatedQsViews.clear();
mQQSTileHeightAnimator = null;
mOtherFirstPageTilesHeightAnimator = null;
mNumQuickTiles = mQuickQsPanel.getNumQuickTiles();
QSTileLayout tileLayout = mQsPanelController.getTileLayout();
mAllViews.add((View) tileLayout);
mLastQQSTileHeight = 0;
if (mQsPanelController.areThereTiles()) {
for (QSTile tile : tiles) {
QSTileView tileView = mQsPanelController.getTileView(tile);
if (tileView == null) {
Log.e(TAG, "tileView is null " + tile.getTileSpec());
continue;
}
// Only animate tiles in the first page
if (mPagedLayout != null && count >= mPagedLayout.getNumTilesFirstPage()) {
break;
}
final View tileIcon = tileView.getIcon().getIconView();
View view = mQs.getView();
// This case: less tiles to animate in small displays.
if (count < mQuickQSPanelController.getTileLayout().getNumVisibleTiles()) {
// Quick tiles.
QSTileView quickTileView = mQuickQSPanelController.getTileView(tile);
if (quickTileView == null) continue;
getRelativePosition(mTmpLoc1, quickTileView, view);
getRelativePosition(mTmpLoc2, tileView, view);
int yOffset = mTmpLoc2[1] - mTmpLoc1[1];
int xOffset = mTmpLoc2[0] - mTmpLoc1[0];
// Offset the translation animation on the views
// (that goes from 0 to getOffsetTranslation)
qqsTranslationYBuilder.addFloat(quickTileView, "translationY", 0, yOffset);
translationYBuilder.addFloat(tileView, "translationY", -yOffset, 0);
translationXBuilder.addFloat(quickTileView, "translationX", 0, xOffset);
translationXBuilder.addFloat(tileView, "translationX", -xOffset, 0);
if (mQQSTileHeightAnimator == null) {
mQQSTileHeightAnimator = new HeightExpansionAnimator(this,
quickTileView.getMeasuredHeight(), tileView.getMeasuredHeight());
mLastQQSTileHeight = quickTileView.getMeasuredHeight();
}
mQQSTileHeightAnimator.addView(quickTileView);
// Icons
translateContent(
quickTileView.getIcon(),
tileView.getIcon(),
view,
xOffset,
yOffset,
mTmpLoc1,
translationXBuilder,
translationYBuilder,
qqsTranslationYBuilder
);
// Label containers
translateContent(
quickTileView.getLabelContainer(),
tileView.getLabelContainer(),
view,
xOffset,
yOffset,
mTmpLoc1,
translationXBuilder,
translationYBuilder,
qqsTranslationYBuilder
);
// Secondary icon
translateContent(
quickTileView.getSecondaryIcon(),
tileView.getSecondaryIcon(),
view,
xOffset,
yOffset,
mTmpLoc1,
translationXBuilder,
translationYBuilder,
qqsTranslationYBuilder
);
// Secondary labels on tiles not in QQS have two alpha animation applied:
// * on the tile themselves
// * on TileLayout
// Therefore, we use a quadratic interpolator animator to animate the alpha
// for tiles in QQS to match.
quadraticInterpolatorBuilder
.addFloat(quickTileView.getSecondaryLabel(), "alpha", 0, 1);
nonFirstPageAlphaBuilder
.addFloat(quickTileView.getSecondaryLabel(), "alpha", 0, 0);
mAnimatedQsViews.add(tileView);
mAllViews.add(quickTileView);
mAllViews.add(quickTileView.getSecondaryLabel());
} else if (!isIconInAnimatedRow(count)) {
// Pretend there's a corresponding QQS tile (for the position) that we are
// expanding from.
SideLabelTileLayout qqsLayout =
(SideLabelTileLayout) mQuickQsPanel.getTileLayout();
getRelativePosition(mTmpLoc1, qqsLayout, view);
mQQSTop = mTmpLoc1[1];
getRelativePosition(mTmpLoc2, tileView, view);
int diff = mTmpLoc2[1] - (mTmpLoc1[1] + qqsLayout.getPhantomTopPosition(count));
translationYBuilder.addFloat(tileView, "translationY", -diff, 0);
if (mOtherFirstPageTilesHeightAnimator == null) {
mOtherFirstPageTilesHeightAnimator =
new HeightExpansionAnimator(
this, mLastQQSTileHeight, tileView.getMeasuredHeight());
}
mOtherFirstPageTilesHeightAnimator.addView(tileView);
tileView.setClipChildren(true);
tileView.setClipToPadding(true);
firstPageBuilder.addFloat(tileView.getSecondaryLabel(), "alpha", 0, 1);
mAllViews.add(tileView.getSecondaryLabel());
}
mAllViews.add(tileView);
count++;
}
if (mCurrentPage != 0) {
addNonFirstPageAnimators(mCurrentPage);
}
}
animateBrightnessSlider();
mFirstPageAnimator = firstPageBuilder
// Fade in the tiles/labels as we reach the final position.
.addFloat(tileLayout, "alpha", 0, 1)
.addFloat(quadraticInterpolatorBuilder.build(), "position", 0, 1)
.setListener(this)
.build();
// Fade in the media player as we reach the final position
Builder builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY);
if (mQsPanelController.shouldUseHorizontalLayout()
&& mQsPanelController.mMediaHost.hostView != null) {
builder.addFloat(mQsPanelController.mMediaHost.hostView, "alpha", 0, 1);
} else {
// In portrait, media view should always be visible
mQsPanelController.mMediaHost.hostView.setAlpha(1.0f);
}
mAllPagesDelayedAnimator = builder.build();
translationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator());
qqsTranslationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator());
translationXBuilder.setInterpolator(mQSExpansionPathInterpolator.getXInterpolator());
if (mOnFirstPage) {
// Only recreate this animator if we're in the first page. That way we know that
// the first page is attached and has the proper positions/measures.
mQQSTranslationYAnimator = qqsTranslationYBuilder.build();
}
mTranslationYAnimator = translationYBuilder.build();
mTranslationXAnimator = translationXBuilder.build();
if (mQQSTileHeightAnimator != null) {
mQQSTileHeightAnimator.setInterpolator(
mQSExpansionPathInterpolator.getYInterpolator());
}
if (mOtherFirstPageTilesHeightAnimator != null) {
mOtherFirstPageTilesHeightAnimator.setInterpolator(
mQSExpansionPathInterpolator.getYInterpolator());
}
mNonfirstPageAlphaAnimator = nonFirstPageAlphaBuilder
.addFloat(mQuickQsPanel, "alpha", 1, 0)
.addFloat(tileLayout, "alpha", 0, 1)
.setListener(mNonFirstPageListener)
.setEndDelay(1 - QQS_FADE_IN_INTERVAL)
.build();
}
private Pair<HeightExpansionAnimator, TouchAnimator> createSecondaryPageAnimators(int page) {
if (mPagedLayout == null) return null;
HeightExpansionAnimator animator = null;
TouchAnimator.Builder builder = new Builder()
.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator());
TouchAnimator.Builder alphaDelayedBuilder = new Builder()
.setStartDelay(QS_TILE_LABEL_FADE_OUT_START)
.setEndDelay(QS_TILE_LABEL_FADE_OUT_END);
SideLabelTileLayout qqsLayout = (SideLabelTileLayout) mQuickQsPanel.getTileLayout();
View view = mQs.getView();
List<String> specs = mPagedLayout.getSpecsForPage(page);
if (specs.isEmpty()) {
// specs should not be empty in a valid secondary page, as we scrolled to it.
// We may crash later on because there's a null animator.
specs = mHost.getSpecs();
Log.e(TAG, "Trying to create animators for empty page " + page + ". Tiles: " + specs);
// return null;
}
int row = -1;
int lastTileTop = -1;
for (int i = 0; i < specs.size(); i++) {
QSTileView tileView = mQsPanelController.getTileView(specs.get(i));
getRelativePosition(mTmpLoc2, tileView, view);
int diff = mTmpLoc2[1] - (mQQSTop + qqsLayout.getPhantomTopPosition(i));
builder.addFloat(tileView, "translationY", -diff, 0);
// The different elements in the tile should be centered, so maintain them centered
int centerDiff = (tileView.getMeasuredHeight() - mLastQQSTileHeight) / 2;
builder.addFloat(tileView.getIcon(), "translationY", -centerDiff, 0);
builder.addFloat(tileView.getSecondaryIcon(), "translationY", -centerDiff, 0);
// The labels have different apparent size in QQS vs QS (no secondary label), so the
// translation needs to account for that.
int secondaryLabelOffset = 0;
if (tileView.getSecondaryLabel().getVisibility() == View.VISIBLE) {
secondaryLabelOffset = tileView.getSecondaryLabel().getMeasuredHeight() / 2;
}
int labelDiff = centerDiff - secondaryLabelOffset;
builder.addFloat(tileView.getLabelContainer(), "translationY", -labelDiff, 0);
builder.addFloat(tileView.getSecondaryLabel(), "alpha", 0, 0.3f, 1);
alphaDelayedBuilder.addFloat(tileView.getLabelContainer(), "alpha", 0, 1);
alphaDelayedBuilder.addFloat(tileView.getIcon(), "alpha", 0, 1);
alphaDelayedBuilder.addFloat(tileView.getSecondaryIcon(), "alpha", 0, 1);
final int tileTop = tileView.getTop();
if (tileTop != lastTileTop) {
row++;
lastTileTop = tileTop;
}
if (i >= mQuickQsPanel.getTileLayout().getNumVisibleTiles() && row >= 2) {
// Fade completely the tiles in rows below the ones that will merge into QQS.
// args is an array of 0s where the length is the current row index (at least third
// row)
final float[] args = new float[row];
args[args.length - 1] = 1f;
builder.addFloat(tileView, "alpha", args);
} else {
// For all the other rows, fade them a bit
builder.addFloat(tileView, "alpha", 0.6f, 1);
}
if (animator == null) {
animator = new HeightExpansionAnimator(
this, mLastQQSTileHeight, tileView.getMeasuredHeight());
animator.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator());
}
animator.addView(tileView);
tileView.setClipChildren(true);
tileView.setClipToPadding(true);
mAllViews.add(tileView);
mAllViews.add(tileView.getSecondaryLabel());
mAllViews.add(tileView.getIcon());
mAllViews.add(tileView.getSecondaryIcon());
mAllViews.add(tileView.getLabelContainer());
}
builder.addFloat(alphaDelayedBuilder.build(), "position", 0, 1);
return new Pair<>(animator, builder.build());
}
private void animateBrightnessSlider() {
mBrightnessTranslationAnimator = null;
mBrightnessOpacityAnimator = null;
View qsBrightness = mQsPanelController.getBrightnessView();
View qqsBrightness = mQuickQSPanelController.getBrightnessView();
if (qqsBrightness != null && qqsBrightness.getVisibility() == View.VISIBLE) {
// animating in split shade mode
mAnimatedQsViews.add(qsBrightness);
mAllViews.add(qqsBrightness);
int translationY = getRelativeTranslationY(qsBrightness, qqsBrightness);
mBrightnessTranslationAnimator = new Builder()
// we need to animate qs brightness even if animation will not be visible,
// as we might start from sliderScaleY set to 0.3 if device was in collapsed QS
// portrait orientation before
.addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
.addFloat(qqsBrightness, "translationY", 0, translationY)
.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator())
.build();
} else if (qsBrightness != null) {
// The brightness slider's visible bottom edge must maintain a constant margin from the
// QS tiles during transition. Thus the slider must (1) perform the same vertical
// translation as the tiles, and (2) compensate for the slider scaling.
// For (1), compute the distance via the vertical distance between QQS and QS tile
// layout top.
View quickSettingsRootView = mQs.getView();
View qsTileLayout = (View) mQsPanelController.getTileLayout();
View qqsTileLayout = (View) mQuickQSPanelController.getTileLayout();
getRelativePosition(mTmpLoc1, qsTileLayout, quickSettingsRootView);
getRelativePosition(mTmpLoc2, qqsTileLayout, quickSettingsRootView);
int tileMovement = mTmpLoc2[1] - mTmpLoc1[1];
// For (2), the slider scales to the vertical center, so compensate with half the
// height at full collapse.
float scaleCompensation = qsBrightness.getMeasuredHeight() * 0.5f;
mBrightnessTranslationAnimator = new Builder()
.addFloat(qsBrightness, "translationY", scaleCompensation + tileMovement, 0)
.addFloat(qsBrightness, "sliderScaleY", 0, 1)
.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator())
.build();
// While the slider's position and unfurl is animated throughouth the motion, the
// fade in happens independently.
mBrightnessOpacityAnimator = new Builder()
.addFloat(qsBrightness, "alpha", 0, 1)
.setStartDelay(0.2f)
.setEndDelay(1 - 0.5f)
.build();
mAllViews.add(qsBrightness);
}
}
private int getRelativeTranslationY(View view1, View view2) {
int[] qsPosition = new int[2];
int[] qqsPosition = new int[2];
View commonView = mQs.getView();
getRelativePositionInt(qsPosition, view1, commonView);
getRelativePositionInt(qqsPosition, view2, commonView);
return qsPosition[1] - qqsPosition[1];
}
private boolean isIconInAnimatedRow(int count) {
if (mPagedLayout == null) {
return false;
}
final int columnCount = mPagedLayout.getColumnCount();
return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount;
}
private void getRelativePosition(int[] loc1, View view, View parent) {
loc1[0] = 0 + view.getWidth() / 2;
loc1[1] = 0;
getRelativePositionInt(loc1, view, parent);
}
private void getRelativePositionInt(int[] loc1, View view, View parent) {
if (view == parent || view == null) return;
// Ignore tile pages as they can have some offset we don't want to take into account in
// RTL.
if (!isAPage(view)) {
loc1[0] += view.getLeft();
loc1[1] += view.getTop();
}
if (!(view instanceof PagedTileLayout)) {
// Remove the scrolling position of all scroll views other than the viewpager
loc1[0] -= view.getScrollX();
loc1[1] -= view.getScrollY();
}
getRelativePositionInt(loc1, (View) view.getParent(), parent);
}
// Returns true if the view is a possible page in PagedTileLayout
private boolean isAPage(View view) {
return view.getClass().equals(SideLabelTileLayout.class);
}
public void setPosition(float position) {
if (mNeedsAnimatorUpdate) {
updateAnimators();
}
if (mFirstPageAnimator == null) return;
if (mOnKeyguard) {
if (mShowCollapsedOnKeyguard) {
position = 0;
} else {
position = 1;
}
}
mLastPosition = position;
if (mOnFirstPage) {
mQuickQsPanel.setAlpha(1);
mFirstPageAnimator.setPosition(position);
mTranslationYAnimator.setPosition(position);
mTranslationXAnimator.setPosition(position);
if (mOtherFirstPageTilesHeightAnimator != null) {
mOtherFirstPageTilesHeightAnimator.setPosition(position);
}
} else {
mNonfirstPageAlphaAnimator.setPosition(position);
}
for (int i = 0; i < mNonFirstPageQSAnimators.size(); i++) {
Pair<HeightExpansionAnimator, TouchAnimator> pair = mNonFirstPageQSAnimators.valueAt(i);
if (pair != null) {
pair.first.setPosition(position);
pair.second.setPosition(position);
}
}
if (mQQSTileHeightAnimator != null) {
mQQSTileHeightAnimator.setPosition(position);
}
mQQSTranslationYAnimator.setPosition(position);
mAllPagesDelayedAnimator.setPosition(position);
if (mBrightnessOpacityAnimator != null) {
mBrightnessOpacityAnimator.setPosition(position);
}
if (mBrightnessTranslationAnimator != null) {
mBrightnessTranslationAnimator.setPosition(position);
}
if (mQQSFooterActionsAnimator != null) {
mQQSFooterActionsAnimator.setPosition(position);
}
}
@Override
public void onAnimationAtStart() {
mQuickQsPanel.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationAtEnd() {
mQuickQsPanel.setVisibility(View.INVISIBLE);
final int N = mAnimatedQsViews.size();
for (int i = 0; i < N; i++) {
mAnimatedQsViews.get(i).setVisibility(View.VISIBLE);
}
}
@Override
public void onAnimationStarted() {
updateQQSVisibility();
if (mOnFirstPage) {
final int N = mAnimatedQsViews.size();
for (int i = 0; i < N; i++) {
mAnimatedQsViews.get(i).setVisibility(View.INVISIBLE);
}
}
}
private void clearAnimationState() {
final int N = mAllViews.size();
mQuickQsPanel.setAlpha(0);
for (int i = 0; i < N; i++) {
View v = mAllViews.get(i);
v.setAlpha(1);
v.setTranslationX(0);
v.setTranslationY(0);
v.setScaleY(1f);
if (v instanceof SideLabelTileLayout) {
((SideLabelTileLayout) v).setClipChildren(false);
((SideLabelTileLayout) v).setClipToPadding(false);
}
}
if (mQQSTileHeightAnimator != null) {
mQQSTileHeightAnimator.resetViewsHeights();
}
if (mOtherFirstPageTilesHeightAnimator != null) {
mOtherFirstPageTilesHeightAnimator.resetViewsHeights();
}
for (int i = 0; i < mNonFirstPageQSAnimators.size(); i++) {
mNonFirstPageQSAnimators.valueAt(i).first.resetViewsHeights();
}
final int N2 = mAnimatedQsViews.size();
for (int i = 0; i < N2; i++) {
mAnimatedQsViews.get(i).setVisibility(View.VISIBLE);
}
}
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
boolean actualChange =
left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom;
if (actualChange) mExecutor.execute(mUpdateAnimators);
}
@Override
public void onTilesChanged() {
// Give the QS panels a moment to generate their new tiles, then create all new animators
// hooked up to the new views.
mExecutor.execute(mUpdateAnimators);
}
private final TouchAnimator.Listener mNonFirstPageListener =
new TouchAnimator.ListenerAdapter() {
@Override
public void onAnimationAtEnd() {
mQuickQsPanel.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationStarted() {
mQuickQsPanel.setVisibility(View.VISIBLE);
}
};
private final Runnable mUpdateAnimators = () -> {
updateAnimators();
setCurrentPosition();
};
private static class HeightExpansionAnimator {
private final List<View> mViews = new ArrayList<>();
private final ValueAnimator mAnimator;
private final TouchAnimator.Listener mListener;
private final ValueAnimator.AnimatorUpdateListener mUpdateListener =
new ValueAnimator.AnimatorUpdateListener() {
float mLastT = -1;
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float t = valueAnimator.getAnimatedFraction();
final int viewCount = mViews.size();
int height = (Integer) valueAnimator.getAnimatedValue();
for (int i = 0; i < viewCount; i++) {
View v = mViews.get(i);
if (v instanceof HeightOverrideable) {
((HeightOverrideable) v).setHeightOverride(height);
} else {
v.setBottom(v.getTop() + height);
}
}
if (t == 0f) {
mListener.onAnimationAtStart();
} else if (t == 1f) {
mListener.onAnimationAtEnd();
} else if (mLastT <= 0 || mLastT == 1) {
mListener.onAnimationStarted();
}
mLastT = t;
}
};
HeightExpansionAnimator(TouchAnimator.Listener listener, int startHeight, int endHeight) {
mListener = listener;
mAnimator = ValueAnimator.ofInt(startHeight, endHeight);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.setRepeatMode(ValueAnimator.REVERSE);
mAnimator.addUpdateListener(mUpdateListener);
}
void addView(View v) {
mViews.add(v);
}
void setInterpolator(TimeInterpolator interpolator) {
mAnimator.setInterpolator(interpolator);
}
void setPosition(float position) {
mAnimator.setCurrentFraction(position);
}
void resetViewsHeights() {
final int viewsCount = mViews.size();
for (int i = 0; i < viewsCount; i++) {
View v = mViews.get(i);
if (v instanceof HeightOverrideable) {
((HeightOverrideable) v).resetOverride();
} else {
v.setBottom(v.getTop() + v.getMeasuredHeight());
}
}
}
}
}