blob: ea3a60b932466afa6fb65b1a9ad7e6fcbc656d18 [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;
Jason Monk6573ef22016-04-06 12:37:18 -04009import android.content.res.Configuration;
10import android.content.res.Resources;
Jason Monk51fb85a2016-03-28 14:06:04 -040011import android.support.v4.view.PagerAdapter;
12import android.support.v4.view.ViewPager;
Jason Monkcaf37622015-08-18 12:33:50 -040013import android.util.AttributeSet;
14import android.util.Log;
15import android.view.LayoutInflater;
Amin Shaikha07a17b2018-02-23 16:02:52 -050016import android.view.MotionEvent;
Jason Monkcaf37622015-08-18 12:33:50 -040017import android.view.View;
18import android.view.ViewGroup;
Amin Shaikha07a17b2018-02-23 16:02:52 -050019import android.view.animation.Interpolator;
20import android.view.animation.OvershootInterpolator;
21import android.widget.Scroller;
Jason Monkdf36aed2016-07-25 11:21:56 -040022
Jason Monkcaf37622015-08-18 12:33:50 -040023import com.android.systemui.R;
24import com.android.systemui.qs.QSPanel.QSTileLayout;
25import com.android.systemui.qs.QSPanel.TileRecord;
26
27import java.util.ArrayList;
Amin Shaikha07a17b2018-02-23 16:02:52 -050028import java.util.Set;
Jason Monkcaf37622015-08-18 12:33:50 -040029
30public class PagedTileLayout extends ViewPager implements QSTileLayout {
31
32 private static final boolean DEBUG = false;
33
34 private static final String TAG = "PagedTileLayout";
Amin Shaikha07a17b2018-02-23 16:02:52 -050035 private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
36 private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
37 private static final long BOUNCE_ANIMATION_DURATION = 450L;
38 private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
39 private static final Interpolator SCROLL_CUBIC = (t) -> {
40 t -= 1.0f;
41 return t * t * t + 1.0f;
42 };
43
Jason Monkcaf37622015-08-18 12:33:50 -040044
45 private final ArrayList<TileRecord> mTiles = new ArrayList<TileRecord>();
46 private final ArrayList<TilePage> mPages = new ArrayList<TilePage>();
47
Jason Monkcaf37622015-08-18 12:33:50 -040048 private PageIndicator mPageIndicator;
49
50 private int mNumPages;
Jason Monk162011e2016-02-19 08:11:55 -050051 private PageListener mPageListener;
Jason Monkcaf37622015-08-18 12:33:50 -040052
Jason Monke5107a32016-05-31 15:40:58 -040053 private int mPosition;
54 private boolean mOffPage;
55 private boolean mListening;
Amin Shaikha07a17b2018-02-23 16:02:52 -050056 private Scroller mScroller;
57
58 private AnimatorSet mBounceAnimatorSet;
59 private int mAnimatingToPage = -1;
Jason Monke5107a32016-05-31 15:40:58 -040060
Jason Monkcaf37622015-08-18 12:33:50 -040061 public PagedTileLayout(Context context, AttributeSet attrs) {
62 super(context, attrs);
Amin Shaikha07a17b2018-02-23 16:02:52 -050063 mScroller = new Scroller(context, SCROLL_CUBIC);
Jason Monkcaf37622015-08-18 12:33:50 -040064 setAdapter(mAdapter);
Amin Shaikha07a17b2018-02-23 16:02:52 -050065 setOnPageChangeListener(mOnPageChangeListener);
66 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -040067 }
68
69 @Override
Jason Monk51fb85a2016-03-28 14:06:04 -040070 public void onRtlPropertiesChanged(int layoutDirection) {
71 super.onRtlPropertiesChanged(layoutDirection);
72 setAdapter(mAdapter);
73 setCurrentItem(0, false);
74 }
75
76 @Override
77 public void setCurrentItem(int item, boolean smoothScroll) {
78 if (isLayoutRtl()) {
79 item = mPages.size() - 1 - item;
80 }
81 super.setCurrentItem(item, smoothScroll);
82 }
83
84 @Override
Jason Monke5107a32016-05-31 15:40:58 -040085 public void setListening(boolean listening) {
86 if (mListening == listening) return;
87 mListening = listening;
88 if (mListening) {
Jason Monk69dac2b2016-09-30 10:15:16 -040089 setPageListening(mPosition, true);
Jason Monke5107a32016-05-31 15:40:58 -040090 if (mOffPage) {
Jason Monk69dac2b2016-09-30 10:15:16 -040091 setPageListening(mPosition + 1, true);
Jason Monke5107a32016-05-31 15:40:58 -040092 }
93 } else {
94 // Make sure no pages are listening.
95 for (int i = 0; i < mPages.size(); i++) {
96 mPages.get(i).setListening(false);
97 }
98 }
99 }
100
Amin Shaikha07a17b2018-02-23 16:02:52 -0500101 @Override
102 public boolean onInterceptTouchEvent(MotionEvent ev) {
103 // Suppress all touch event during reveal animation.
104 if (mAnimatingToPage != -1) {
105 return true;
106 }
107 return super.onInterceptTouchEvent(ev);
108 }
109
110 @Override
111 public boolean onTouchEvent(MotionEvent ev) {
112 // Suppress all touch event during reveal animation.
113 if (mAnimatingToPage != -1) {
114 return true;
115 }
116 return super.onTouchEvent(ev);
117 }
118
119 @Override
120 public void computeScroll() {
121 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
122 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
123 float pageFraction = (float) getScrollX() / getWidth();
124 int position = (int) pageFraction;
125 float positionOffset = pageFraction - position;
126 mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX());
127 // Keep on drawing until the animation has finished.
128 postInvalidateOnAnimation();
129 return;
130 }
131 if (mAnimatingToPage != -1) {
132 setCurrentItem(mAnimatingToPage, true);
133 mBounceAnimatorSet.start();
134 setOffscreenPageLimit(1);
135 mAnimatingToPage = -1;
136 }
137 super.computeScroll();
138 }
139
Jason Monke5107a32016-05-31 15:40:58 -0400140 /**
141 * Sets individual pages to listening or not. If offPage it will set
142 * the next page after position to listening as well since we are in between
143 * pages.
144 */
145 private void setCurrentPage(int position, boolean offPage) {
146 if (mPosition == position && mOffPage == offPage) return;
147 if (mListening) {
148 if (mPosition != position) {
149 // Clear out the last pages from listening.
Jason Monk45233d62016-06-09 09:59:56 -0400150 setPageListening(mPosition, false);
Jason Monke5107a32016-05-31 15:40:58 -0400151 if (mOffPage) {
Jason Monk45233d62016-06-09 09:59:56 -0400152 setPageListening(mPosition + 1, false);
Jason Monke5107a32016-05-31 15:40:58 -0400153 }
154 // Set the new pages to listening
Jason Monk45233d62016-06-09 09:59:56 -0400155 setPageListening(position, true);
Jason Monke5107a32016-05-31 15:40:58 -0400156 if (offPage) {
Jason Monk45233d62016-06-09 09:59:56 -0400157 setPageListening(position + 1, true);
Jason Monke5107a32016-05-31 15:40:58 -0400158 }
159 } else if (mOffPage != offPage) {
160 // Whether we are showing position + 1 has changed.
Jason Monk45233d62016-06-09 09:59:56 -0400161 setPageListening(mPosition + 1, offPage);
Jason Monke5107a32016-05-31 15:40:58 -0400162 }
163 }
164 // Save the current state.
165 mPosition = position;
166 mOffPage = offPage;
167 }
168
Jason Monk45233d62016-06-09 09:59:56 -0400169 private void setPageListening(int position, boolean listening) {
170 if (position >= mPages.size()) return;
Jason Monk69dac2b2016-09-30 10:15:16 -0400171 if (isLayoutRtl()) {
172 position = mPages.size() - 1 - position;
173 }
Jason Monk45233d62016-06-09 09:59:56 -0400174 mPages.get(position).setListening(listening);
175 }
176
Jason Monke5107a32016-05-31 15:40:58 -0400177 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500178 public boolean hasOverlappingRendering() {
179 return false;
180 }
181
182 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400183 protected void onFinishInflate() {
184 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500185 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500186 .inflate(R.layout.qs_paged_page, this, false));
Jason Monkcaf37622015-08-18 12:33:50 -0400187 }
188
Jason Monk32508852017-01-18 09:17:13 -0500189 public void setPageIndicator(PageIndicator indicator) {
190 mPageIndicator = indicator;
191 }
192
Jason Monkcaf37622015-08-18 12:33:50 -0400193 @Override
194 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500195 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
196 if (parent == null) return 0;
197 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400198 }
199
200 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400201 public void addTile(TileRecord tile) {
202 mTiles.add(tile);
Jason Monk9d02a432016-01-20 16:33:46 -0500203 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400204 }
205
206 @Override
207 public void removeTile(TileRecord tile) {
208 if (mTiles.remove(tile)) {
Jason Monk9d02a432016-01-20 16:33:46 -0500209 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400210 }
211 }
212
Jason Monk162011e2016-02-19 08:11:55 -0500213 public void setPageListener(PageListener listener) {
214 mPageListener = listener;
215 }
216
Jason Monk9d02a432016-01-20 16:33:46 -0500217 private void postDistributeTiles() {
218 removeCallbacks(mDistribute);
219 post(mDistribute);
220 }
221
Jason Monkcaf37622015-08-18 12:33:50 -0400222 private void distributeTiles() {
223 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400224 final int NP = mPages.size();
225 for (int i = 0; i < NP; i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500226 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400227 }
228 int index = 0;
229 final int NT = mTiles.size();
230 for (int i = 0; i < NT; i++) {
231 TileRecord tile = mTiles.get(i);
Jason Monkcaf37622015-08-18 12:33:50 -0400232 if (mPages.get(index).isFull()) {
233 if (++index == mPages.size()) {
Jason Monk8e73ef32016-05-04 11:36:46 -0400234 if (DEBUG) Log.d(TAG, "Adding page for "
235 + tile.tile.getClass().getSimpleName());
Jason Monk32508852017-01-18 09:17:13 -0500236 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monkcaf37622015-08-18 12:33:50 -0400237 .inflate(R.layout.qs_paged_page, this, false));
238 }
239 }
240 if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
241 + index);
242 mPages.get(index).addTile(tile);
243 }
244 if (mNumPages != index + 1) {
245 mNumPages = index + 1;
Jason Monk8e73ef32016-05-04 11:36:46 -0400246 while (mPages.size() > mNumPages) {
247 mPages.remove(mPages.size() - 1);
248 }
249 if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
Jason Monkcaf37622015-08-18 12:33:50 -0400250 mPageIndicator.setNumPages(mNumPages);
Jason Monk32508852017-01-18 09:17:13 -0500251 mPageIndicator.setVisibility(mNumPages > 1 ? View.VISIBLE : View.GONE);
Jason Monk8e73ef32016-05-04 11:36:46 -0400252 setAdapter(mAdapter);
Jason Monkcaf37622015-08-18 12:33:50 -0400253 mAdapter.notifyDataSetChanged();
Jason Monk51fb85a2016-03-28 14:06:04 -0400254 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400255 }
256 }
257
258 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500259 public boolean updateResources() {
260 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400261 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500262 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400263 }
Jason Monk9d02a432016-01-20 16:33:46 -0500264 if (changed) {
265 distributeTiles();
266 }
267 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400268 }
269
270 @Override
271 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
272 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
273 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
274 // of the pages.
275 int maxHeight = 0;
276 final int N = getChildCount();
277 for (int i = 0; i < N; i++) {
278 int height = getChildAt(i).getMeasuredHeight();
279 if (height > maxHeight) {
280 maxHeight = height;
281 }
282 }
Jason Monkf13413e2017-02-15 15:49:32 -0500283 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400284 }
285
Jason Monk9d02a432016-01-20 16:33:46 -0500286 private final Runnable mDistribute = new Runnable() {
287 @Override
288 public void run() {
289 distributeTiles();
290 }
291 };
292
Jason Monk8fb77872016-03-03 16:39:42 -0500293 public int getColumnCount() {
294 if (mPages.size() == 0) return 0;
295 return mPages.get(0).mColumns;
296 }
297
Amin Shaikha07a17b2018-02-23 16:02:52 -0500298 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
299 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) {
300 // Do not start the reveal animation unless there are tiles to animate, multiple
301 // TilePages available and the user has not already started dragging.
302 return;
303 }
304
305 final int lastPageNumber = mPages.size() - 1;
306 final TilePage lastPage = mPages.get(lastPageNumber);
307 final ArrayList<Animator> bounceAnims = new ArrayList<>();
308 for (TileRecord tr : lastPage.mRecords) {
309 if (tileSpecs.contains(tr.tile.getTileSpec())) {
310 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
311 }
312 }
313
314 if (bounceAnims.isEmpty()) {
315 // All tileSpecs are on the first page. Nothing to do.
316 // TODO: potentially show a bounce animation for first page QS tiles
317 return;
318 }
319
320 mBounceAnimatorSet = new AnimatorSet();
321 mBounceAnimatorSet.playTogether(bounceAnims);
322 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
323 @Override
324 public void onAnimationEnd(Animator animation) {
325 mBounceAnimatorSet = null;
326 postAnimation.run();
327 }
328 });
329 mAnimatingToPage = lastPageNumber;
330 setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated.
331 mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0,
332 REVEAL_SCROLL_DURATION_MILLIS);
333 postInvalidateOnAnimation();
334 }
335
336 private static Animator setupBounceAnimator(View view, int ordinal) {
337 view.setAlpha(0f);
338 view.setScaleX(0f);
339 view.setScaleY(0f);
340 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
341 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
342 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
343 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
344 animator.setDuration(BOUNCE_ANIMATION_DURATION);
345 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
346 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
347 return animator;
348 }
349
350 private final ViewPager.OnPageChangeListener mOnPageChangeListener =
351 new ViewPager.SimpleOnPageChangeListener() {
352 @Override
353 public void onPageSelected(int position) {
354 if (mPageIndicator == null) return;
355 if (mPageListener != null) {
356 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
357 : position == 0);
358 }
359 }
360
361 @Override
362 public void onPageScrolled(int position, float positionOffset,
363 int positionOffsetPixels) {
364 if (mPageIndicator == null) return;
365 setCurrentPage(position, positionOffset != 0);
366 mPageIndicator.setLocation(position + positionOffset);
367 if (mPageListener != null) {
368 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
369 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
370 }
371 }
372 };
373
Jason Monkcaf37622015-08-18 12:33:50 -0400374 public static class TilePage extends TileLayout {
Jason Monk94a1bf62015-10-20 08:43:36 -0700375 private int mMaxRows = 3;
Jason Monkcaf37622015-08-18 12:33:50 -0400376 public TilePage(Context context, AttributeSet attrs) {
377 super(context, attrs);
Jason Monkb9c00192015-10-07 11:45:33 -0400378 updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400379 }
380
Jason Monk9d02a432016-01-20 16:33:46 -0500381 @Override
382 public boolean updateResources() {
Jason Monk6573ef22016-04-06 12:37:18 -0400383 final int rows = getRows();
384 boolean changed = rows != mMaxRows;
385 if (changed) {
386 mMaxRows = rows;
387 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500388 }
Jason Monk6573ef22016-04-06 12:37:18 -0400389 return super.updateResources() || changed;
390 }
391
392 private int getRows() {
393 final Resources res = getContext().getResources();
394 if (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
Jason Monk290a4192017-04-21 11:51:17 -0400395 return res.getInteger(R.integer.quick_settings_num_rows_portrait);
Jason Monk6573ef22016-04-06 12:37:18 -0400396 }
397 return Math.max(1, res.getInteger(R.integer.quick_settings_num_rows));
Jason Monk9d02a432016-01-20 16:33:46 -0500398 }
399
Jason Monkbd6dbb02015-09-03 15:46:25 -0400400 public void setMaxRows(int maxRows) {
401 mMaxRows = maxRows;
402 }
403
Jason Monkbd6dbb02015-09-03 15:46:25 -0400404 public boolean isFull() {
Jason Monkcaf37622015-08-18 12:33:50 -0400405 return mRecords.size() >= mColumns * mMaxRows;
406 }
407 }
408
409 private final PagerAdapter mAdapter = new PagerAdapter() {
410 public void destroyItem(ViewGroup container, int position, Object object) {
411 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400412 container.removeView((View) object);
413 }
414
415 public Object instantiateItem(ViewGroup container, int position) {
416 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400417 if (isLayoutRtl()) {
418 position = mPages.size() - 1 - position;
419 }
Jason Monke4e69302016-01-20 11:27:15 -0500420 ViewGroup view = mPages.get(position);
Jason Monkcaf37622015-08-18 12:33:50 -0400421 container.addView(view);
422 return view;
423 }
424
425 @Override
426 public int getCount() {
427 return mNumPages;
428 }
429
430 @Override
431 public boolean isViewFromObject(View view, Object object) {
432 return view == object;
433 }
434 };
Jason Monk162011e2016-02-19 08:11:55 -0500435
436 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500437 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500438 }
Jason Monkcaf37622015-08-18 12:33:50 -0400439}