blob: 7afa82a245516a442ce26d19dd45a6d8b9c721c8 [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;
Aurimas Liutikasfd52c142018-04-17 09:50:46 -070011import androidx.viewpager.widget.PagerAdapter;
12import androidx.viewpager.widget.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
Amin Shaikh9978c552018-03-22 07:24:41 -040045 private final ArrayList<TileRecord> mTiles = new ArrayList<>();
46 private final ArrayList<TilePage> mPages = new ArrayList<>();
Jason Monkcaf37622015-08-18 12:33:50 -040047
Jason Monkcaf37622015-08-18 12:33:50 -040048 private PageIndicator mPageIndicator;
Rohan Shah3090e792018-04-12 00:01:00 -040049 private float mPageIndicatorPosition;
Jason Monkcaf37622015-08-18 12:33:50 -040050
51 private int mNumPages;
Jason Monk162011e2016-02-19 08:11:55 -050052 private PageListener mPageListener;
Jason Monkcaf37622015-08-18 12:33:50 -040053
Jason Monke5107a32016-05-31 15:40:58 -040054 private boolean mListening;
Amin Shaikha07a17b2018-02-23 16:02:52 -050055 private Scroller mScroller;
56
57 private AnimatorSet mBounceAnimatorSet;
58 private int mAnimatingToPage = -1;
Amin Shaikh4c9048c2018-04-20 11:27:46 -040059 private float mLastExpansion;
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;
Amin Shaikh9978c552018-03-22 07:24:41 -040088 updateListening();
89 }
90
91 private void updateListening() {
92 for (TilePage tilePage : mPages) {
93 tilePage.setListening(tilePage.getParent() == null ? false : mListening);
Jason Monke5107a32016-05-31 15:40:58 -040094 }
95 }
96
Amin Shaikha07a17b2018-02-23 16:02:52 -050097 @Override
98 public boolean onInterceptTouchEvent(MotionEvent ev) {
99 // Suppress all touch event during reveal animation.
100 if (mAnimatingToPage != -1) {
101 return true;
102 }
103 return super.onInterceptTouchEvent(ev);
104 }
105
106 @Override
107 public boolean onTouchEvent(MotionEvent ev) {
108 // Suppress all touch event during reveal animation.
109 if (mAnimatingToPage != -1) {
110 return true;
111 }
112 return super.onTouchEvent(ev);
113 }
114
115 @Override
116 public void computeScroll() {
117 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
118 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
119 float pageFraction = (float) getScrollX() / getWidth();
120 int position = (int) pageFraction;
121 float positionOffset = pageFraction - position;
122 mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX());
123 // Keep on drawing until the animation has finished.
124 postInvalidateOnAnimation();
125 return;
126 }
127 if (mAnimatingToPage != -1) {
128 setCurrentItem(mAnimatingToPage, true);
129 mBounceAnimatorSet.start();
130 setOffscreenPageLimit(1);
131 mAnimatingToPage = -1;
132 }
133 super.computeScroll();
134 }
135
Jason Monke5107a32016-05-31 15:40:58 -0400136 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500137 public boolean hasOverlappingRendering() {
138 return false;
139 }
140
141 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400142 protected void onFinishInflate() {
143 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500144 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500145 .inflate(R.layout.qs_paged_page, this, false));
Jason Monkcaf37622015-08-18 12:33:50 -0400146 }
147
Jason Monk32508852017-01-18 09:17:13 -0500148 public void setPageIndicator(PageIndicator indicator) {
149 mPageIndicator = indicator;
Rohan Shah3090e792018-04-12 00:01:00 -0400150 mPageIndicator.setNumPages(mNumPages);
151 mPageIndicator.setLocation(mPageIndicatorPosition);
Jason Monk32508852017-01-18 09:17:13 -0500152 }
153
Jason Monkcaf37622015-08-18 12:33:50 -0400154 @Override
155 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500156 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
157 if (parent == null) return 0;
158 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400159 }
160
161 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400162 public void addTile(TileRecord tile) {
163 mTiles.add(tile);
Jason Monk9d02a432016-01-20 16:33:46 -0500164 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400165 }
166
167 @Override
168 public void removeTile(TileRecord tile) {
169 if (mTiles.remove(tile)) {
Jason Monk9d02a432016-01-20 16:33:46 -0500170 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400171 }
172 }
173
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400174 @Override
175 public void setExpansion(float expansion) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400176 mLastExpansion = expansion;
177 updateSelected();
178 }
179
180 private void updateSelected() {
181 // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
182 // other expansion ratios since there is no way way to pause the marquee.
183 if (mLastExpansion > 0f && mLastExpansion < 1f) {
184 return;
185 }
186 boolean selected = mLastExpansion == 1f;
187 for (int i = 0; i < mPages.size(); i++) {
188 mPages.get(i).setSelected(i == getCurrentItem() ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400189 }
190 }
191
Jason Monk162011e2016-02-19 08:11:55 -0500192 public void setPageListener(PageListener listener) {
193 mPageListener = listener;
194 }
195
Jason Monk9d02a432016-01-20 16:33:46 -0500196 private void postDistributeTiles() {
197 removeCallbacks(mDistribute);
198 post(mDistribute);
199 }
200
Jason Monkcaf37622015-08-18 12:33:50 -0400201 private void distributeTiles() {
202 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400203 final int NP = mPages.size();
204 for (int i = 0; i < NP; i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500205 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400206 }
207 int index = 0;
208 final int NT = mTiles.size();
209 for (int i = 0; i < NT; i++) {
210 TileRecord tile = mTiles.get(i);
Jason Monkcaf37622015-08-18 12:33:50 -0400211 if (mPages.get(index).isFull()) {
212 if (++index == mPages.size()) {
Jason Monk8e73ef32016-05-04 11:36:46 -0400213 if (DEBUG) Log.d(TAG, "Adding page for "
214 + tile.tile.getClass().getSimpleName());
Jason Monk32508852017-01-18 09:17:13 -0500215 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monkcaf37622015-08-18 12:33:50 -0400216 .inflate(R.layout.qs_paged_page, this, false));
217 }
218 }
219 if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
220 + index);
221 mPages.get(index).addTile(tile);
222 }
223 if (mNumPages != index + 1) {
224 mNumPages = index + 1;
Jason Monk8e73ef32016-05-04 11:36:46 -0400225 while (mPages.size() > mNumPages) {
226 mPages.remove(mPages.size() - 1);
227 }
228 if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
Jason Monkcaf37622015-08-18 12:33:50 -0400229 mPageIndicator.setNumPages(mNumPages);
Jason Monk8e73ef32016-05-04 11:36:46 -0400230 setAdapter(mAdapter);
Jason Monkcaf37622015-08-18 12:33:50 -0400231 mAdapter.notifyDataSetChanged();
Jason Monk51fb85a2016-03-28 14:06:04 -0400232 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400233 }
234 }
235
236 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500237 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400238 // Update bottom padding, useful for removing extra space once the panel page indicator is
239 // hidden.
240 setPadding(0, 0, 0,
241 getContext().getResources().getDimensionPixelSize(
242 R.dimen.qs_paged_tile_layout_padding_bottom));
243
Jason Monk9d02a432016-01-20 16:33:46 -0500244 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400245 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500246 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400247 }
Jason Monk9d02a432016-01-20 16:33:46 -0500248 if (changed) {
249 distributeTiles();
250 }
251 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400252 }
253
254 @Override
255 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
256 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
257 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
258 // of the pages.
259 int maxHeight = 0;
260 final int N = getChildCount();
261 for (int i = 0; i < N; i++) {
262 int height = getChildAt(i).getMeasuredHeight();
263 if (height > maxHeight) {
264 maxHeight = height;
265 }
266 }
Jason Monkf13413e2017-02-15 15:49:32 -0500267 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400268 }
269
Jason Monk9d02a432016-01-20 16:33:46 -0500270 private final Runnable mDistribute = new Runnable() {
271 @Override
272 public void run() {
273 distributeTiles();
274 }
275 };
276
Jason Monk8fb77872016-03-03 16:39:42 -0500277 public int getColumnCount() {
278 if (mPages.size() == 0) return 0;
279 return mPages.get(0).mColumns;
280 }
281
Amin Shaikha07a17b2018-02-23 16:02:52 -0500282 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
283 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) {
284 // Do not start the reveal animation unless there are tiles to animate, multiple
285 // TilePages available and the user has not already started dragging.
286 return;
287 }
288
289 final int lastPageNumber = mPages.size() - 1;
290 final TilePage lastPage = mPages.get(lastPageNumber);
291 final ArrayList<Animator> bounceAnims = new ArrayList<>();
292 for (TileRecord tr : lastPage.mRecords) {
293 if (tileSpecs.contains(tr.tile.getTileSpec())) {
294 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
295 }
296 }
297
298 if (bounceAnims.isEmpty()) {
299 // All tileSpecs are on the first page. Nothing to do.
300 // TODO: potentially show a bounce animation for first page QS tiles
301 return;
302 }
303
304 mBounceAnimatorSet = new AnimatorSet();
305 mBounceAnimatorSet.playTogether(bounceAnims);
306 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
307 @Override
308 public void onAnimationEnd(Animator animation) {
309 mBounceAnimatorSet = null;
310 postAnimation.run();
311 }
312 });
313 mAnimatingToPage = lastPageNumber;
314 setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated.
315 mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0,
316 REVEAL_SCROLL_DURATION_MILLIS);
317 postInvalidateOnAnimation();
318 }
319
320 private static Animator setupBounceAnimator(View view, int ordinal) {
321 view.setAlpha(0f);
322 view.setScaleX(0f);
323 view.setScaleY(0f);
324 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
325 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
326 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
327 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
328 animator.setDuration(BOUNCE_ANIMATION_DURATION);
329 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
330 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
331 return animator;
332 }
333
334 private final ViewPager.OnPageChangeListener mOnPageChangeListener =
335 new ViewPager.SimpleOnPageChangeListener() {
336 @Override
337 public void onPageSelected(int position) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400338 updateSelected();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500339 if (mPageIndicator == null) return;
340 if (mPageListener != null) {
341 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
342 : position == 0);
343 }
344 }
345
346 @Override
347 public void onPageScrolled(int position, float positionOffset,
348 int positionOffsetPixels) {
349 if (mPageIndicator == null) return;
Rohan Shah3090e792018-04-12 00:01:00 -0400350 mPageIndicatorPosition = position + positionOffset;
351 mPageIndicator.setLocation(mPageIndicatorPosition);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500352 if (mPageListener != null) {
353 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
354 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
355 }
356 }
357 };
358
Jason Monkcaf37622015-08-18 12:33:50 -0400359 public static class TilePage extends TileLayout {
Jason Monk94a1bf62015-10-20 08:43:36 -0700360 private int mMaxRows = 3;
Jason Monkcaf37622015-08-18 12:33:50 -0400361 public TilePage(Context context, AttributeSet attrs) {
362 super(context, attrs);
Jason Monkb9c00192015-10-07 11:45:33 -0400363 updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400364 }
365
Jason Monk9d02a432016-01-20 16:33:46 -0500366 @Override
367 public boolean updateResources() {
Jason Monk6573ef22016-04-06 12:37:18 -0400368 final int rows = getRows();
369 boolean changed = rows != mMaxRows;
370 if (changed) {
371 mMaxRows = rows;
372 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500373 }
Jason Monk6573ef22016-04-06 12:37:18 -0400374 return super.updateResources() || changed;
375 }
376
377 private int getRows() {
Amin Shaikh78d834b2018-04-23 13:07:31 -0400378 return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
Jason Monk9d02a432016-01-20 16:33:46 -0500379 }
380
Jason Monkbd6dbb02015-09-03 15:46:25 -0400381 public void setMaxRows(int maxRows) {
382 mMaxRows = maxRows;
383 }
384
Jason Monkbd6dbb02015-09-03 15:46:25 -0400385 public boolean isFull() {
Jason Monkcaf37622015-08-18 12:33:50 -0400386 return mRecords.size() >= mColumns * mMaxRows;
387 }
388 }
389
390 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400391 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400392 public void destroyItem(ViewGroup container, int position, Object object) {
393 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400394 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400395 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400396 }
397
Amin Shaikh9978c552018-03-22 07:24:41 -0400398 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400399 public Object instantiateItem(ViewGroup container, int position) {
400 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400401 if (isLayoutRtl()) {
402 position = mPages.size() - 1 - position;
403 }
Jason Monke4e69302016-01-20 11:27:15 -0500404 ViewGroup view = mPages.get(position);
Jason Monkcaf37622015-08-18 12:33:50 -0400405 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400406 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400407 return view;
408 }
409
410 @Override
411 public int getCount() {
412 return mNumPages;
413 }
414
415 @Override
416 public boolean isViewFromObject(View view, Object object) {
417 return view == object;
418 }
419 };
Jason Monk162011e2016-02-19 08:11:55 -0500420
421 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500422 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500423 }
Jason Monkcaf37622015-08-18 12:33:50 -0400424}