From a07a17bcf39bc1acde886f91a2aa12491b2e3b2b Mon Sep 17 00:00:00 2001 From: Amin Shaikh Date: Fri, 23 Feb 2018 16:02:52 -0500 Subject: [PATCH] Import launcher style reveal animations to QS. When a new QS tile is added automatically (not through user customization), the next time the user pulls down QS, reveal this new tile in the same style as the launcher. Only play this animation once for each new tile added. This reveal animation will only run if the new tile is not on the first page in QS. Bug: 73741556 Test: visual Change-Id: I8f642d8fd51f63f999eb3f811c13c40f2bea60fa --- .../src/com/android/systemui/Prefs.java | 11 ++ .../android/systemui/qs/PagedTileLayout.java | 169 +++++++++++++++--- .../com/android/systemui/qs/QSFragment.java | 1 + .../src/com/android/systemui/qs/QSPanel.java | 12 +- .../systemui/qs/QSTileRevealController.java | 76 ++++++++ 5 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index ee573fbeaf337..396d317e89545 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -24,6 +24,7 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; +import java.util.Set; public final class Prefs { private Prefs() {} // no instantation @@ -87,6 +88,7 @@ public final class Prefs { String NUM_APPS_LAUNCHED = "NumAppsLaunched"; String HAS_SEEN_RECENTS_ONBOARDING = "HasSeenRecentsOnboarding"; String SEEN_RINGER_GUIDANCE_COUNT = "RingerGuidanceCount"; + String QS_TILE_SPECS_REVEALED = "QsTileSpecsRevealed"; } public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) { @@ -121,6 +123,15 @@ public final class Prefs { get(context).edit().putString(key, value).apply(); } + public static void putStringSet(Context context, @Key String key, Set value) { + get(context).edit().putStringSet(key, value).apply(); + } + + public static Set getStringSet( + Context context, @Key String key, Set defaultValue) { + return get(context).getStringSet(key, defaultValue); + } + public static Map getAll(Context context) { return get(context).getAll(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index f3417dc078c29..ea3a60b932466 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -1,5 +1,10 @@ package com.android.systemui.qs; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -8,20 +13,34 @@ import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.Scroller; import com.android.systemui.R; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanel.TileRecord; import java.util.ArrayList; +import java.util.Set; public class PagedTileLayout extends ViewPager implements QSTileLayout { private static final boolean DEBUG = false; private static final String TAG = "PagedTileLayout"; + private static final int REVEAL_SCROLL_DURATION_MILLIS = 750; + private static final float BOUNCE_ANIMATION_TENSION = 1.3f; + private static final long BOUNCE_ANIMATION_DURATION = 450L; + private static final int TILE_ANIMATION_STAGGER_DELAY = 85; + private static final Interpolator SCROLL_CUBIC = (t) -> { + t -= 1.0f; + return t * t * t + 1.0f; + }; + private final ArrayList mTiles = new ArrayList(); private final ArrayList mPages = new ArrayList(); @@ -34,37 +53,17 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private int mPosition; private boolean mOffPage; private boolean mListening; + private Scroller mScroller; + + private AnimatorSet mBounceAnimatorSet; + private int mAnimatingToPage = -1; public PagedTileLayout(Context context, AttributeSet attrs) { super(context, attrs); + mScroller = new Scroller(context, SCROLL_CUBIC); setAdapter(mAdapter); - setOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageSelected(int position) { - if (mPageIndicator == null) return; - if (mPageListener != null) { - mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1 - : position == 0); - } - } - - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - if (mPageIndicator == null) return; - setCurrentPage(position, positionOffset != 0); - mPageIndicator.setLocation(position + positionOffset); - if (mPageListener != null) { - mPageListener.onPageChanged(positionOffsetPixels == 0 && - (isLayoutRtl() ? position == mPages.size() - 1 : position == 0)); - } - } - - @Override - public void onPageScrollStateChanged(int state) { - } - }); - setCurrentItem(0); + setOnPageChangeListener(mOnPageChangeListener); + setCurrentItem(0, false); } @Override @@ -99,6 +98,45 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { } } + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Suppress all touch event during reveal animation. + if (mAnimatingToPage != -1) { + return true; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // Suppress all touch event during reveal animation. + if (mAnimatingToPage != -1) { + return true; + } + return super.onTouchEvent(ev); + } + + @Override + public void computeScroll() { + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + float pageFraction = (float) getScrollX() / getWidth(); + int position = (int) pageFraction; + float positionOffset = pageFraction - position; + mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX()); + // Keep on drawing until the animation has finished. + postInvalidateOnAnimation(); + return; + } + if (mAnimatingToPage != -1) { + setCurrentItem(mAnimatingToPage, true); + mBounceAnimatorSet.start(); + setOffscreenPageLimit(1); + mAnimatingToPage = -1; + } + super.computeScroll(); + } + /** * Sets individual pages to listening or not. If offPage it will set * the next page after position to listening as well since we are in between @@ -257,9 +295,84 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { return mPages.get(0).mColumns; } + public void startTileReveal(Set tileSpecs, final Runnable postAnimation) { + if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) { + // Do not start the reveal animation unless there are tiles to animate, multiple + // TilePages available and the user has not already started dragging. + return; + } + + final int lastPageNumber = mPages.size() - 1; + final TilePage lastPage = mPages.get(lastPageNumber); + final ArrayList bounceAnims = new ArrayList<>(); + for (TileRecord tr : lastPage.mRecords) { + if (tileSpecs.contains(tr.tile.getTileSpec())) { + bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size())); + } + } + + if (bounceAnims.isEmpty()) { + // All tileSpecs are on the first page. Nothing to do. + // TODO: potentially show a bounce animation for first page QS tiles + return; + } + + mBounceAnimatorSet = new AnimatorSet(); + mBounceAnimatorSet.playTogether(bounceAnims); + mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBounceAnimatorSet = null; + postAnimation.run(); + } + }); + mAnimatingToPage = lastPageNumber; + setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated. + mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0, + REVEAL_SCROLL_DURATION_MILLIS); + postInvalidateOnAnimation(); + } + + private static Animator setupBounceAnimator(View view, int ordinal) { + view.setAlpha(0f); + view.setScaleX(0f); + view.setScaleY(0f); + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, + PropertyValuesHolder.ofFloat(View.ALPHA, 1), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1)); + animator.setDuration(BOUNCE_ANIMATION_DURATION); + animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY); + animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION)); + return animator; + } + + private final ViewPager.OnPageChangeListener mOnPageChangeListener = + new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + if (mPageIndicator == null) return; + if (mPageListener != null) { + mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1 + : position == 0); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + if (mPageIndicator == null) return; + setCurrentPage(position, positionOffset != 0); + mPageIndicator.setLocation(position + positionOffset); + if (mPageListener != null) { + mPageListener.onPageChanged(positionOffsetPixels == 0 && + (isLayoutRtl() ? position == mPages.size() - 1 : position == 0)); + } + } + }; + public static class TilePage extends TileLayout { private int mMaxRows = 3; - public TilePage(Context context, AttributeSet attrs) { super(context, attrs); updateResources(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 5758762b06a03..29f3c43a1fa4e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -290,6 +290,7 @@ public class QSFragment extends Fragment implements QS { // Let the views animate their contents correctly by giving them the necessary context. mHeader.setExpansion(mKeyguardShowing, expansion, panelTranslationY); mFooter.setExpansion(mKeyguardShowing ? 1 : expansion); + mQSPanel.getQsTileRevealController().setExpansion(expansion); mQSPanel.setTranslationY(translationScaleY * heightDiff); mQSDetail.setFullyExpanded(fullyExpanded); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 143ad21c998c0..61e3065fd4a36 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -60,11 +60,12 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public static final String QS_SHOW_HEADER = "qs_show_header"; protected final Context mContext; - protected final ArrayList mRecords = new ArrayList(); + protected final ArrayList mRecords = new ArrayList<>(); protected final View mBrightnessView; private final H mHandler = new H(); private final View mPageIndicator; private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); + private final QSTileRevealController mQsTileRevealController; protected boolean mExpanded; protected boolean mListening; @@ -108,6 +109,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne addView(mPageIndicator); ((PagedTileLayout) mTileLayout).setPageIndicator((PageIndicator) mPageIndicator); + mQsTileRevealController = new QSTileRevealController(mContext, this, + ((PagedTileLayout) mTileLayout)); addDivider(); @@ -136,6 +139,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return mPageIndicator; } + public QSTileRevealController getQsTileRevealController() { + return mQsTileRevealController; + } + public boolean isShowingCustomize() { return mCustomizePanel != null && mCustomizePanel.isCustomizing(); } @@ -352,6 +359,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } public void setTiles(Collection tiles, boolean collapsedView) { + if (!collapsedView) { + mQsTileRevealController.updateRevealedTiles(tiles); + } for (TileRecord record : mRecords) { mTileLayout.removeTile(record); record.tile.removeCallback(record.callback); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java new file mode 100644 index 0000000000000..2f012e6e608ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java @@ -0,0 +1,76 @@ +package com.android.systemui.qs; + +import static com.android.systemui.Prefs.Key.QS_TILE_SPECS_REVEALED; + +import android.content.Context; +import android.os.Handler; +import android.util.ArraySet; + +import com.android.systemui.Prefs; +import com.android.systemui.plugins.qs.QSTile; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public class QSTileRevealController { + private static final long QS_REVEAL_TILES_DELAY = 500L; + + private final Context mContext; + private final QSPanel mQSPanel; + private final PagedTileLayout mPagedTileLayout; + private final ArraySet mTilesToReveal = new ArraySet<>(); + private final Handler mHandler = new Handler(); + + private final Runnable mRevealQsTiles = new Runnable() { + @Override + public void run() { + mPagedTileLayout.startTileReveal(mTilesToReveal, () -> { + if (mQSPanel.isExpanded()) { + addTileSpecsToRevealed(mTilesToReveal); + mTilesToReveal.clear(); + } + }); + } + }; + + QSTileRevealController(Context context, QSPanel qsPanel, PagedTileLayout pagedTileLayout) { + mContext = context; + mQSPanel = qsPanel; + mPagedTileLayout = pagedTileLayout; + } + + public void setExpansion(float expansion) { + if (expansion == 1f) { + mHandler.postDelayed(mRevealQsTiles, QS_REVEAL_TILES_DELAY); + } else { + mHandler.removeCallbacks(mRevealQsTiles); + } + } + + public void updateRevealedTiles(Collection tiles) { + ArraySet tileSpecs = new ArraySet<>(); + for (QSTile tile : tiles) { + tileSpecs.add(tile.getTileSpec()); + } + + final Set revealedTiles = Prefs.getStringSet( + mContext, QS_TILE_SPECS_REVEALED, Collections.EMPTY_SET); + if (revealedTiles.isEmpty() || mQSPanel.isShowingCustomize()) { + // Do not reveal QS tiles the user has upon first load or those that they directly + // added through customization. + addTileSpecsToRevealed(tileSpecs); + } else { + // Animate all tiles that the user has not directly added themselves. + tileSpecs.removeAll(revealedTiles); + mTilesToReveal.addAll(tileSpecs); + } + } + + private void addTileSpecsToRevealed(ArraySet specs) { + final ArraySet revealedTiles = new ArraySet<>( + Prefs.getStringSet(mContext, QS_TILE_SPECS_REVEALED, Collections.EMPTY_SET)); + revealedTiles.addAll(specs); + Prefs.putStringSet(mContext, QS_TILE_SPECS_REVEALED, revealedTiles); + } +}