blob: ad8058498ffd05d27134f309bdc0821bfd3461ad [file] [log] [blame]
Jason Monkcaf37622015-08-18 12:33:50 -04001package com.android.systemui.qs;
2
Amin Shaikha07a17b2018-02-23 16:02:52 -05003import android.animation.Animator;
4import android.animation.AnimatorListenerAdapter;
5import android.animation.AnimatorSet;
6import android.animation.ObjectAnimator;
7import android.animation.PropertyValuesHolder;
Jason Monkcaf37622015-08-18 12:33:50 -04008import android.content.Context;
Fabian Kozynski6eaf3ac2018-10-01 15:59:05 -04009import android.content.res.Resources;
Jason Monkcaf37622015-08-18 12:33:50 -040010import android.util.AttributeSet;
11import android.util.Log;
12import android.view.LayoutInflater;
13import android.view.View;
14import android.view.ViewGroup;
Amin Shaikha07a17b2018-02-23 16:02:52 -050015import android.view.animation.Interpolator;
16import android.view.animation.OvershootInterpolator;
17import android.widget.Scroller;
Jason Monkdf36aed2016-07-25 11:21:56 -040018
Fabian Kozynski6eaf3ac2018-10-01 15:59:05 -040019import androidx.viewpager.widget.PagerAdapter;
20import androidx.viewpager.widget.ViewPager;
21
Jason Monkcaf37622015-08-18 12:33:50 -040022import com.android.systemui.R;
23import com.android.systemui.qs.QSPanel.QSTileLayout;
24import com.android.systemui.qs.QSPanel.TileRecord;
25
26import java.util.ArrayList;
Amin Shaikha07a17b2018-02-23 16:02:52 -050027import java.util.Set;
Jason Monkcaf37622015-08-18 12:33:50 -040028
29public class PagedTileLayout extends ViewPager implements QSTileLayout {
30
31 private static final boolean DEBUG = false;
32
33 private static final String TAG = "PagedTileLayout";
Amin Shaikha07a17b2018-02-23 16:02:52 -050034 private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
35 private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
36 private static final long BOUNCE_ANIMATION_DURATION = 450L;
37 private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
38 private static final Interpolator SCROLL_CUBIC = (t) -> {
39 t -= 1.0f;
40 return t * t * t + 1.0f;
41 };
42
Amin Shaikh9978c552018-03-22 07:24:41 -040043 private final ArrayList<TileRecord> mTiles = new ArrayList<>();
44 private final ArrayList<TilePage> mPages = new ArrayList<>();
Jason Monkcaf37622015-08-18 12:33:50 -040045
Jason Monkcaf37622015-08-18 12:33:50 -040046 private PageIndicator mPageIndicator;
Rohan Shah3090e792018-04-12 00:01:00 -040047 private float mPageIndicatorPosition;
Jason Monkcaf37622015-08-18 12:33:50 -040048
Jason Monk162011e2016-02-19 08:11:55 -050049 private PageListener mPageListener;
Jason Monkcaf37622015-08-18 12:33:50 -040050
Jason Monke5107a32016-05-31 15:40:58 -040051 private boolean mListening;
Amin Shaikha07a17b2018-02-23 16:02:52 -050052 private Scroller mScroller;
53
54 private AnimatorSet mBounceAnimatorSet;
Amin Shaikh4c9048c2018-04-20 11:27:46 -040055 private float mLastExpansion;
Fabian Kozynski712ae392018-09-12 09:11:16 -040056 private boolean mDistributeTiles = false;
Jason Monke5107a32016-05-31 15:40:58 -040057
Jason Monkcaf37622015-08-18 12:33:50 -040058 public PagedTileLayout(Context context, AttributeSet attrs) {
59 super(context, attrs);
Amin Shaikha07a17b2018-02-23 16:02:52 -050060 mScroller = new Scroller(context, SCROLL_CUBIC);
Jason Monkcaf37622015-08-18 12:33:50 -040061 setAdapter(mAdapter);
Amin Shaikha07a17b2018-02-23 16:02:52 -050062 setOnPageChangeListener(mOnPageChangeListener);
63 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -040064 }
65
66 @Override
Jason Monk51fb85a2016-03-28 14:06:04 -040067 public void onRtlPropertiesChanged(int layoutDirection) {
68 super.onRtlPropertiesChanged(layoutDirection);
69 setAdapter(mAdapter);
70 setCurrentItem(0, false);
71 }
72
73 @Override
74 public void setCurrentItem(int item, boolean smoothScroll) {
75 if (isLayoutRtl()) {
76 item = mPages.size() - 1 - item;
77 }
78 super.setCurrentItem(item, smoothScroll);
79 }
80
81 @Override
Jason Monke5107a32016-05-31 15:40:58 -040082 public void setListening(boolean listening) {
83 if (mListening == listening) return;
84 mListening = listening;
Amin Shaikh9978c552018-03-22 07:24:41 -040085 updateListening();
86 }
87
88 private void updateListening() {
89 for (TilePage tilePage : mPages) {
90 tilePage.setListening(tilePage.getParent() == null ? false : mListening);
Jason Monke5107a32016-05-31 15:40:58 -040091 }
92 }
93
Amin Shaikha07a17b2018-02-23 16:02:52 -050094 @Override
Amin Shaikha07a17b2018-02-23 16:02:52 -050095 public void computeScroll() {
96 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
Amin Shaikh5484d0f2018-07-31 10:44:11 -040097 fakeDragBy(getScrollX() - mScroller.getCurrX());
Amin Shaikha07a17b2018-02-23 16:02:52 -050098 // Keep on drawing until the animation has finished.
99 postInvalidateOnAnimation();
100 return;
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400101 } else if (isFakeDragging()) {
102 endFakeDrag();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500103 mBounceAnimatorSet.start();
104 setOffscreenPageLimit(1);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500105 }
106 super.computeScroll();
107 }
108
Jason Monke5107a32016-05-31 15:40:58 -0400109 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500110 public boolean hasOverlappingRendering() {
111 return false;
112 }
113
114 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400115 protected void onFinishInflate() {
116 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500117 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500118 .inflate(R.layout.qs_paged_page, this, false));
Fabian Kozynskifae42a82018-10-04 11:18:42 -0400119 mAdapter.notifyDataSetChanged();
Jason Monkcaf37622015-08-18 12:33:50 -0400120 }
121
Jason Monk32508852017-01-18 09:17:13 -0500122 public void setPageIndicator(PageIndicator indicator) {
123 mPageIndicator = indicator;
Fabian Kozynski712ae392018-09-12 09:11:16 -0400124 mPageIndicator.setNumPages(mPages.size());
Rohan Shah3090e792018-04-12 00:01:00 -0400125 mPageIndicator.setLocation(mPageIndicatorPosition);
Jason Monk32508852017-01-18 09:17:13 -0500126 }
127
Jason Monkcaf37622015-08-18 12:33:50 -0400128 @Override
129 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500130 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
131 if (parent == null) return 0;
132 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400133 }
134
135 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400136 public void addTile(TileRecord tile) {
137 mTiles.add(tile);
Fabian Kozynski712ae392018-09-12 09:11:16 -0400138 mDistributeTiles = true;
139 requestLayout();
Jason Monkcaf37622015-08-18 12:33:50 -0400140 }
141
142 @Override
143 public void removeTile(TileRecord tile) {
144 if (mTiles.remove(tile)) {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400145 mDistributeTiles = true;
146 requestLayout();
Jason Monkcaf37622015-08-18 12:33:50 -0400147 }
148 }
149
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400150 @Override
151 public void setExpansion(float expansion) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400152 mLastExpansion = expansion;
153 updateSelected();
154 }
155
156 private void updateSelected() {
157 // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
158 // other expansion ratios since there is no way way to pause the marquee.
159 if (mLastExpansion > 0f && mLastExpansion < 1f) {
160 return;
161 }
162 boolean selected = mLastExpansion == 1f;
Rohan Shah2dbcb572018-05-25 10:51:22 -0700163
164 // Disable accessibility temporarily while we update selected state purely for the
165 // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
166 // event on any of the children.
167 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Amin Shaikh6f570742018-06-01 17:18:19 -0400168 int currentItem = isLayoutRtl() ? mPages.size() - 1 - getCurrentItem() : getCurrentItem();
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400169 for (int i = 0; i < mPages.size(); i++) {
Amin Shaikh6f570742018-06-01 17:18:19 -0400170 mPages.get(i).setSelected(i == currentItem ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400171 }
Rohan Shah2dbcb572018-05-25 10:51:22 -0700172 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400173 }
174
Jason Monk162011e2016-02-19 08:11:55 -0500175 public void setPageListener(PageListener listener) {
176 mPageListener = listener;
177 }
178
Jason Monkcaf37622015-08-18 12:33:50 -0400179 private void distributeTiles() {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400180 emptyAndInflateOrRemovePages();
181
182 final int tileCount = mPages.get(0).maxTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400183 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400184 int index = 0;
185 final int NT = mTiles.size();
186 for (int i = 0; i < NT; i++) {
187 TileRecord tile = mTiles.get(i);
Fabian Kozynski712ae392018-09-12 09:11:16 -0400188 if (mPages.get(index).mRecords.size() == tileCount) index++;
189 if (DEBUG) {
190 Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
191 + index);
Jason Monkcaf37622015-08-18 12:33:50 -0400192 }
Jason Monkcaf37622015-08-18 12:33:50 -0400193 mPages.get(index).addTile(tile);
194 }
Fabian Kozynski712ae392018-09-12 09:11:16 -0400195 }
196
197 private void emptyAndInflateOrRemovePages() {
198 final int nTiles = mTiles.size();
199 int numPages = nTiles / mPages.get(0).maxTiles();
200 // Add one more not full page if needed
201 numPages += (nTiles % mPages.get(0).maxTiles() == 0 ? 0 : 1);
202
203 final int NP = mPages.size();
204 for (int i = 0; i < NP; i++) {
205 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400206 }
Fabian Kozynski712ae392018-09-12 09:11:16 -0400207 if (NP == numPages) {
208 return;
209 }
210 while (mPages.size() < numPages) {
211 if (DEBUG) Log.d(TAG, "Adding page");
212 mPages.add((TilePage) LayoutInflater.from(getContext())
213 .inflate(R.layout.qs_paged_page, this, false));
214 }
215 while (mPages.size() > numPages) {
216 if (DEBUG) Log.d(TAG, "Removing page");
217 mPages.remove(mPages.size() - 1);
218 }
219 mPageIndicator.setNumPages(mPages.size());
220 setAdapter(mAdapter);
221 mAdapter.notifyDataSetChanged();
222 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400223 }
224
225 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500226 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400227 // Update bottom padding, useful for removing extra space once the panel page indicator is
228 // hidden.
Fabian Kozynski6eaf3ac2018-10-01 15:59:05 -0400229 Resources res = getContext().getResources();
230 final int sidePadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
231 setPadding(sidePadding, 0, sidePadding,
Rohan Shah3090e792018-04-12 00:01:00 -0400232 getContext().getResources().getDimensionPixelSize(
233 R.dimen.qs_paged_tile_layout_padding_bottom));
Jason Monk9d02a432016-01-20 16:33:46 -0500234 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400235 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500236 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400237 }
Jason Monk9d02a432016-01-20 16:33:46 -0500238 if (changed) {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400239 mDistributeTiles = true;
240 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500241 }
242 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400243 }
244
245 @Override
246 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400247
248 final int nTiles = mTiles.size();
249 if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
250
251 // Only change the pages if the number of rows or columns (from updateResources) has
252 // changed or the tiles have changed
253 if (mPages.get(0).updateMaxRows(heightMeasureSpec, nTiles) || mDistributeTiles) {
254 mDistributeTiles = false;
255 distributeTiles();
256 }
257
258 final int nRows = mPages.get(0).mRows;
259 for (int i = 0; i < mPages.size(); i++) {
260 TilePage t = mPages.get(i);
261 t.mRows = nRows;
262 }
263 }
264
Jason Monkcaf37622015-08-18 12:33:50 -0400265 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Fabian Kozynski712ae392018-09-12 09:11:16 -0400266
Jason Monkcaf37622015-08-18 12:33:50 -0400267 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
268 // of the pages.
269 int maxHeight = 0;
270 final int N = getChildCount();
271 for (int i = 0; i < N; i++) {
272 int height = getChildAt(i).getMeasuredHeight();
273 if (height > maxHeight) {
274 maxHeight = height;
275 }
276 }
Jason Monkf13413e2017-02-15 15:49:32 -0500277 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400278 }
279
Jason Monk8fb77872016-03-03 16:39:42 -0500280 public int getColumnCount() {
281 if (mPages.size() == 0) return 0;
282 return mPages.get(0).mColumns;
283 }
284
Fabian Kozynski802279f2018-09-07 13:44:54 -0400285 public int getNumVisibleTiles() {
286 if (mPages.size() == 0) return 0;
287 TilePage currentPage = mPages.get(getCurrentItem());
288 return currentPage.mRecords.size();
289 }
290
Amin Shaikha07a17b2018-02-23 16:02:52 -0500291 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400292 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0 || !beginFakeDrag()) {
Amin Shaikha07a17b2018-02-23 16:02:52 -0500293 // Do not start the reveal animation unless there are tiles to animate, multiple
294 // TilePages available and the user has not already started dragging.
295 return;
296 }
297
298 final int lastPageNumber = mPages.size() - 1;
299 final TilePage lastPage = mPages.get(lastPageNumber);
300 final ArrayList<Animator> bounceAnims = new ArrayList<>();
301 for (TileRecord tr : lastPage.mRecords) {
302 if (tileSpecs.contains(tr.tile.getTileSpec())) {
303 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
304 }
305 }
306
307 if (bounceAnims.isEmpty()) {
308 // All tileSpecs are on the first page. Nothing to do.
309 // TODO: potentially show a bounce animation for first page QS tiles
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400310 endFakeDrag();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500311 return;
312 }
313
314 mBounceAnimatorSet = new AnimatorSet();
315 mBounceAnimatorSet.playTogether(bounceAnims);
316 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
317 @Override
318 public void onAnimationEnd(Animator animation) {
319 mBounceAnimatorSet = null;
320 postAnimation.run();
321 }
322 });
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400323 setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
324 int dx = getWidth() * lastPageNumber;
325 mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
326 REVEAL_SCROLL_DURATION_MILLIS);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500327 postInvalidateOnAnimation();
328 }
329
330 private static Animator setupBounceAnimator(View view, int ordinal) {
331 view.setAlpha(0f);
332 view.setScaleX(0f);
333 view.setScaleY(0f);
334 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
335 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
336 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
337 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
338 animator.setDuration(BOUNCE_ANIMATION_DURATION);
339 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
340 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
341 return animator;
342 }
343
344 private final ViewPager.OnPageChangeListener mOnPageChangeListener =
345 new ViewPager.SimpleOnPageChangeListener() {
346 @Override
347 public void onPageSelected(int position) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400348 updateSelected();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500349 if (mPageIndicator == null) return;
350 if (mPageListener != null) {
351 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
352 : position == 0);
353 }
354 }
355
356 @Override
357 public void onPageScrolled(int position, float positionOffset,
358 int positionOffsetPixels) {
359 if (mPageIndicator == null) return;
Rohan Shah3090e792018-04-12 00:01:00 -0400360 mPageIndicatorPosition = position + positionOffset;
361 mPageIndicator.setLocation(mPageIndicatorPosition);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500362 if (mPageListener != null) {
363 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
364 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
365 }
366 }
367 };
368
Jason Monkcaf37622015-08-18 12:33:50 -0400369 public static class TilePage extends TileLayout {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400370
Jason Monkcaf37622015-08-18 12:33:50 -0400371 public TilePage(Context context, AttributeSet attrs) {
372 super(context, attrs);
Jason Monkbd6dbb02015-09-03 15:46:25 -0400373 }
374
Jason Monkbd6dbb02015-09-03 15:46:25 -0400375 public boolean isFull() {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400376 return mRecords.size() >= mColumns * mRows;
377 }
378
379 public int maxTiles() {
380 return mColumns * mRows;
Jason Monkcaf37622015-08-18 12:33:50 -0400381 }
382 }
383
384 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400385 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400386 public void destroyItem(ViewGroup container, int position, Object object) {
387 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400388 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400389 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400390 }
391
Amin Shaikh9978c552018-03-22 07:24:41 -0400392 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400393 public Object instantiateItem(ViewGroup container, int position) {
394 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400395 if (isLayoutRtl()) {
396 position = mPages.size() - 1 - position;
397 }
Jason Monke4e69302016-01-20 11:27:15 -0500398 ViewGroup view = mPages.get(position);
Amin Shaikh53bc0832018-09-17 15:06:18 -0400399 if (view.getParent() != null) {
400 container.removeView(view);
401 }
Jason Monkcaf37622015-08-18 12:33:50 -0400402 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400403 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400404 return view;
405 }
406
407 @Override
408 public int getCount() {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400409 return mPages.size();
Jason Monkcaf37622015-08-18 12:33:50 -0400410 }
411
412 @Override
413 public boolean isViewFromObject(View view, Object object) {
414 return view == object;
415 }
416 };
Jason Monk162011e2016-02-19 08:11:55 -0500417
418 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500419 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500420 }
Jason Monkcaf37622015-08-18 12:33:50 -0400421}